├── .gitignore
├── README.md
├── project
├── Build.scala
├── build.properties
└── plugins.sbt
└── src
├── main
└── scala
│ └── com
│ └── typesafe
│ └── sbt
│ └── SbtStartScript.scala
└── sbt-test
└── start
├── 00basic
├── build.sbt
├── project
│ └── plugins.sbt
├── src
│ └── main
│ │ └── scala
│ │ └── Hello.scala
└── test
├── 01jar
├── build.sbt
├── project
│ └── plugins.sbt
├── src
│ └── main
│ │ └── scala
│ │ └── Hello.scala
└── test
└── 02war
├── build.sbt
├── project
└── plugins.sbt
├── src
└── main
│ ├── scala
│ └── Hello.scala
│ └── webapp
│ └── WEB-INF
│ └── web.xml
└── test
/.gitignore:
--------------------------------------------------------------------------------
1 | /project/plugins/target/
2 | /project/target/
3 | /target/
4 | /project/project/target/
5 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## Consider sbt-native-packager instead
2 |
3 | The more general native-packager plugin may replace this one in
4 | the future: https://github.com/sbt/sbt-native-packager
5 |
6 | The rough way to get a start script with sbt-native-packager,
7 | modulo any details of your app, is:
8 |
9 | 1. add sbt-native-packager plugin to your project
10 | 2. remove start-script-plugin
11 | 3. add `settings(com.typesafe.sbt.SbtNativePackager.packageArchetype.java_application: _*)`
12 | 4. `stage` task will now generate a script `target/universal/stage/bin/project-name` instead of `target/start`
13 | 5. the sbt-native-packager-generated script looks at a `java_opts` env var but you cannot pass Java opts as parameters to the script as you could with `target/start`
14 | 6. the sbt-native-packager-generated script copies dependency jars into `target/`, so you don't need the Ivy cache
15 |
16 | Many were using sbt-start-script with Heroku, sbt-native-packager has two tricky things on Heroku right now:
17 |
18 | 1. Heroku sets `JAVA_OPTS` and not `java_opts`. See https://github.com/sbt/sbt-native-packager/issues/47 and https://github.com/sbt/sbt-native-packager/issues/48 ... for now you have to manually configure `java_opts` and not specify memory options, or hack sbt-native-packager.
19 | 2. You need to hack the build pack to drop the Ivy cache, or your slug will be bloated or even exceed the max size.
20 |
21 | Also of course you have to change your `Procfile` for the new name of the script.
22 |
23 | ## About this plugin (sbt-start-script)
24 |
25 | This plugin allows you to generate a script `target/start` for a
26 | project. The script will run the project "in-place" (without having
27 | to build a package first).
28 |
29 | The `target/start` script is similar to `sbt run` but it doesn't rely
30 | on SBT. `sbt run` is not recommended for production use because it
31 | keeps SBT itself in-memory. `target/start` is intended to run an
32 | app in production.
33 |
34 | The plugin adds a task `start-script` which generates `target/start`.
35 | It also adds a `stage` task, aliased to the `start-script` task.
36 |
37 | `stage` by convention performs any tasks needed to prepare an app to
38 | be run in-place. Other plugins that use a different approach to
39 | prepare an app to run could define `stage` as well, while
40 | `start-script` is specific to this plugin.
41 |
42 | The `target/start` script must be run from the root build directory
43 | (note: NOT the root _project_ directory). This allows inter-project
44 | dependencies within your build to work properly.
45 |
46 | ## Details
47 |
48 | To use the plugin with SBT 0.12.x:
49 |
50 | addSbtPlugin("com.typesafe.sbt" % "sbt-start-script" % "0.9.0")
51 |
52 | You can place that code in `~/.sbt/plugins/build.sbt` to install the
53 | plugin globally, or in `YOURPROJECT/project/plugins.sbt` to
54 | install the plugin for your project.
55 |
56 | To use with SBT 0.13.x:
57 |
58 | addSbtPlugin("com.typesafe.sbt" % "sbt-start-script" % "0.10.0")
59 |
60 | Note: the global directory for 0.13.x is `~/.sbt/0.13` instead of `~/.sbt`.
61 |
62 | If you install the plugin globally, it will add a command
63 | `add-start-script-tasks` to every project using SBT. You can run this
64 | command to add the tasks from the plugin, such as `start-script` (the
65 | `start-script` task won't exist until you `add-start-script-tasks`).
66 |
67 | If you incorporate the plugin into your project, then you'll want to
68 | explicitly add the settings from the plugin, such as the
69 | `start-script` task, to your project. In this case there's no need to
70 | use `add-start-script-tasks` since you'll already add them in your
71 | build.
72 |
73 | Here's how you add the settings from the plugin in a `build.sbt`:
74 |
75 | import com.typesafe.sbt.SbtStartScript
76 |
77 | seq(SbtStartScript.startScriptForClassesSettings: _*)
78 |
79 | In an SBT "full configuration" you would do something like:
80 |
81 | settings = SbtStartScript.startScriptForClassesSettings
82 |
83 | You have to choose which settings to add from these options:
84 |
85 | - `startScriptForClassesSettings` (the script will run from .class files)
86 | - `startScriptForJarSettings` (the script will run from .jar file from 'package')
87 | - `startScriptForWarSettings` (the script will run a .war with Jetty)
88 |
89 | `startScriptForWarSettings` requires
90 | https://github.com/siasia/xsbt-web-plugin/ to provide the
91 | `package-war` task.
92 |
93 | If you have an aggregate project, you may want a `stage` task even
94 | though there's nothing to run, just so it will recurse into sub-projects.
95 | One way to get a `stage` task that does nothing is:
96 |
97 | SbtStartScript.stage in Compile := Unit
98 |
99 | which sets the `stage` key to `Unit`.
100 |
101 | ## Key names
102 |
103 | Note that all the keys (except `stage`) are in the
104 | `SbtStartScript.StartScriptKeys` object, so the scala version of
105 | the `start-script` key is
106 | `SbtStartScript.StartScriptKeys.startScript`. This is the standard
107 | convention for sbt plugins. Do an `import
108 | SbtStartScript.StartScriptKeys._` if you want all the keys
109 | unprefixed in your scope. Then, if you want to change a setting, you
110 | can simply reference the key directly in your `build.sbt'.
111 |
112 | For example, to change the filename of the generated script to
113 | something other than `target/start` (which is controlled by the
114 | key `SbtStartScript.StartScriptKeys.startScriptName`), add the
115 | following to `build.sbt` after the above import statement:
116 |
117 | startScriptName <<= target / "run"
118 |
119 | ## Migration from earlier versions of xsbt-start-script-plugin
120 |
121 | After 0.5.2, the plugin and its APIs were renamed to use
122 | consistent conventions (matching other plugins). The renamings
123 | were:
124 |
125 | - the plugin itself is now `sbt-start-script` not
126 | `xsbt-start-script-plugin`; update this in your `plugins.sbt`
127 | - the Maven group and Java package are now `com.typesafe.sbt`
128 | rather than `com.typesafe.startscript`; update this in your
129 | `plugins.sbt` and in your build files
130 | - the plugin object is now `SbtStartScript` rather than
131 | `StartScriptPlugin`, update this in your build files
132 | - if you used any keys directly, they are now inside a nested
133 | object `StartScriptKeys` so for example rather than writing
134 | `startScriptFile` you would write
135 | `StartScriptKeys.startScriptFile` _or_ you need to `import
136 | StartScriptKeys._`
137 | - `StartScriptKeys.startScriptFile` did not match the string
138 | name of that settings `start-script-name` so now you should
139 | use `StartScriptKeys.startScriptName`
140 |
141 | ## License
142 |
143 | sbt-start-script is open source software licensed under the
144 | [Apache 2.0 License](http://www.apache.org/licenses/LICENSE-2.0.html).
145 |
146 | ## Contribution policy
147 |
148 | Contributions via GitHub pull requests are gladly accepted from
149 | their original author. Before sending the pull request, please
150 | agree to the Contributor License Agreement at
151 | http://typesafe.com/contribute/cla (it takes 30 seconds; you use
152 | your GitHub account to sign the agreement).
153 |
--------------------------------------------------------------------------------
/project/Build.scala:
--------------------------------------------------------------------------------
1 | import sbt._
2 |
3 | import Keys._
4 | import Project.Initialize
5 | import com.typesafe.sbt.SbtScalariform
6 | import com.typesafe.sbt.SbtScalariform.ScalariformKeys
7 |
8 | object StartScriptBuild extends Build {
9 | def formatPrefs = {
10 | import scalariform.formatter.preferences._
11 | FormattingPreferences()
12 | .setPreference(IndentSpaces, 4)
13 | }
14 |
15 | lazy val root =
16 | Project("root", file("."), settings = rootSettings)
17 |
18 | lazy val rootSettings = Defaults.defaultSettings ++
19 | ScriptedPlugin.scriptedSettings ++
20 | // formatting
21 | SbtScalariform.scalariformSettings ++ Seq(
22 | ScalariformKeys.preferences in Compile := formatPrefs,
23 | ScalariformKeys.preferences in Test := formatPrefs) ++
24 | Seq(sbtPlugin := true,
25 | organization := "com.typesafe.sbt",
26 | name := "sbt-start-script",
27 | scalacOptions := Seq("-unchecked", "-deprecation"),
28 |
29 | // to release, bump major/minor/micro as appropriate,
30 | // drop SNAPSHOT, tag and publish.
31 | // add snapshot back so git master is a SNAPSHOT.
32 | // when releasing a SNAPSHOT to the repo, bump the micro
33 | // version at least.
34 | // Also, change the version number in the README.md
35 | // Versions and git tags should follow: http://semver.org/
36 | // except using -SNAPSHOT instead of without hyphen.
37 |
38 | version := "0.10.0-SNAPSHOT",
39 | libraryDependencies <++= sbtVersion {
40 | (version) =>
41 | Seq("org.scala-sbt" % "io" % version % "provided",
42 | "org.scala-sbt" % "logging" % version % "provided",
43 | "org.scala-sbt" % "process" % version % "provided")
44 | },
45 |
46 | // publish stuff
47 | publishTo <<= (version) { v =>
48 | def scalasbt(repo: String) = ("scalasbt " + repo, "http://scalasbt.artifactoryonline.com/scalasbt/sbt-plugin-" + repo)
49 | val (name, repo) = if (v.endsWith("-SNAPSHOT")) scalasbt("snapshots") else scalasbt("releases")
50 | Some(Resolver.url(name, url(repo))(Resolver.ivyStylePatterns))
51 | },
52 | publishMavenStyle := false,
53 | credentials += Credentials(Path.userHome / ".ivy2" / ".sbt-credentials"))
54 | }
55 |
--------------------------------------------------------------------------------
/project/build.properties:
--------------------------------------------------------------------------------
1 | sbt.version=0.13.0
2 |
--------------------------------------------------------------------------------
/project/plugins.sbt:
--------------------------------------------------------------------------------
1 | addSbtPlugin("com.typesafe.sbt" % "sbt-scalariform" % "1.2.0")
2 |
3 | libraryDependencies += "org.scala-sbt" % "scripted-plugin" % sbtVersion.value
4 |
--------------------------------------------------------------------------------
/src/main/scala/com/typesafe/sbt/SbtStartScript.scala:
--------------------------------------------------------------------------------
1 | package com.typesafe.sbt
2 |
3 | import _root_.sbt._
4 |
5 | import Project.Initialize
6 | import Keys._
7 | import Defaults._
8 | import Scope.GlobalScope
9 |
10 | import java.util.regex.Pattern
11 | import java.io.File
12 |
13 | object SbtStartScript extends Plugin {
14 | override lazy val settings = Seq(commands += addStartScriptTasksCommand)
15 |
16 | case class RelativeClasspathString(value: String)
17 |
18 | ///// The "stage" setting is generic and may also be used by other plugins
19 | ///// to accomplish staging in a different way (other than a start script)
20 |
21 | val stage = TaskKey[Unit]("stage", "Prepares the project to be run, in environments that deploy source trees rather than packages.")
22 |
23 | ///// Settings keys
24 |
25 | object StartScriptKeys {
26 | val startScriptFile = SettingKey[File]("start-script-name")
27 | // this is newly-added to make the val name consistent with the
28 | // string name, and preferred over startScriptFile
29 | val startScriptName = startScriptFile
30 | val relativeDependencyClasspathString = TaskKey[RelativeClasspathString]("relative-dependency-classpath-string", "Dependency classpath as colon-separated string with each entry relative to the build root directory.")
31 | val relativeFullClasspathString = TaskKey[RelativeClasspathString]("relative-full-classpath-string", "Full classpath as colon-separated string with each entry relative to the build root directory.")
32 | val startScriptBaseDirectory = SettingKey[File]("start-script-base-directory", "All start scripts must be run from this directory.")
33 | val startScriptForWar = TaskKey[File]("start-script-for-war", "Generate a shell script to launch the war file")
34 | val startScriptForJar = TaskKey[File]("start-script-for-jar", "Generate a shell script to launch the jar file")
35 | val startScriptForClasses = TaskKey[File]("start-script-for-classes", "Generate a shell script to launch from classes directory")
36 | val startScriptNotDefined = TaskKey[File]("start-script-not-defined", "Generate a shell script that just complains that the project is not launchable")
37 | val startScript = TaskKey[File]("start-script", "Generate a shell script that runs the application")
38 |
39 | // jetty-related settings keys
40 | val startScriptJettyVersion = SettingKey[String]("start-script-jetty-version", "Version of Jetty to use for running the .war")
41 | val startScriptJettyChecksum = SettingKey[String]("start-script-jetty-checksum", "Expected SHA-1 of the Jetty distribution we intend to download")
42 | val startScriptJettyURL = SettingKey[String]("start-script-jetty-url", "URL of the Jetty distribution to download (if set, then it overrides the start-script-jetty-version)")
43 | val startScriptJettyContextPath = SettingKey[String]("start-script-jetty-context-path", "Context path for the war file when deployed to Jetty")
44 | val startScriptJettyHome = TaskKey[File]("start-script-jetty-home", "Download Jetty distribution and return JETTY_HOME")
45 | }
46 |
47 | import StartScriptKeys._
48 |
49 | // this is in WebPlugin, but we don't want to rely on WebPlugin to build
50 | private val packageWar = TaskKey[File]("package-war")
51 |
52 | // check for OS so a windows-compatible .bat script can be generated
53 | def isWindows(): Boolean = {
54 | val os = System.getProperty("os.name").toLowerCase();
55 | return os.startsWith("windows")
56 | }
57 |
58 | val scriptname: String = if (isWindows()) "start.bat" else "start"
59 |
60 | // apps can manually add these settings (in the way you'd use WebPlugin.webSettings),
61 | // or you can install the plugin globally and use add-start-script-tasks to add
62 | // these settings to any project.
63 | val genericStartScriptSettings: Seq[Project.Setting[_]] = Seq(
64 | startScriptFile <<= (target) { (target) => target / scriptname },
65 | // maybe not the right way to do this...
66 | startScriptBaseDirectory <<= (thisProjectRef) { (ref) => new File(ref.build) },
67 | startScriptNotDefined in Compile <<= (streams, startScriptFile in Compile) map startScriptNotDefinedTask,
68 | relativeDependencyClasspathString in Compile <<= (startScriptBaseDirectory, dependencyClasspath in Runtime) map relativeClasspathStringTask,
69 | relativeFullClasspathString in Compile <<= (startScriptBaseDirectory, fullClasspath in Runtime) map relativeClasspathStringTask,
70 | stage in Compile <<= (startScript in Compile) map stageTask)
71 |
72 | // settings to be added to a web plugin project
73 | val startScriptForWarSettings: Seq[Project.Setting[_]] = Seq(
74 | // hardcoding these defaults is not my favorite, but I'm not sure what else to do exactly.
75 | startScriptJettyVersion in Compile := "7.3.1.v20110307",
76 | startScriptJettyChecksum in Compile := "10cb58096796e2f1d4989590a4263c34ae9419be",
77 | startScriptJettyURL in Compile <<= (startScriptJettyVersion in Compile) { (version) => "http://archive.eclipse.org/jetty/" + version + "/dist/jetty-distribution-" + version + ".zip" },
78 | startScriptJettyContextPath in Compile := "/",
79 | startScriptJettyHome in Compile <<= (streams, target, startScriptJettyURL in Compile, startScriptJettyChecksum in Compile) map startScriptJettyHomeTask,
80 | startScriptForWar in Compile <<= (streams, startScriptBaseDirectory, startScriptFile in Compile, packageWar in Compile, startScriptJettyHome in Compile, startScriptJettyContextPath in Compile) map startScriptForWarTask,
81 | startScript in Compile <<= startScriptForWar in Compile) ++ genericStartScriptSettings
82 |
83 | // settings to be added to a project with an exported jar
84 | val startScriptForJarSettings: Seq[Project.Setting[_]] = Seq(
85 | startScriptForJar in Compile <<= (streams, startScriptBaseDirectory, startScriptFile in Compile, packageBin in Compile, relativeDependencyClasspathString in Compile, mainClass in Compile) map startScriptForJarTask,
86 | startScript in Compile <<= startScriptForJar in Compile) ++ genericStartScriptSettings
87 |
88 | // settings to be added to a project that doesn't export a jar
89 | val startScriptForClassesSettings: Seq[Project.Setting[_]] = Seq(
90 | startScriptForClasses in Compile <<= (streams, startScriptBaseDirectory, startScriptFile in Compile, relativeFullClasspathString in Compile, mainClass in Compile) map startScriptForClassesTask,
91 | startScript in Compile <<= startScriptForClasses in Compile) ++ genericStartScriptSettings
92 |
93 | // Extracted.getOpt is not in 10.1 and earlier
94 | private def inCurrent[T](extracted: Extracted, key: ScopedKey[T]): Scope = {
95 | if (key.scope.project == This)
96 | key.scope.copy(project = Select(extracted.currentRef))
97 | else
98 | key.scope
99 | }
100 | private def getOpt[T](extracted: Extracted, key: ScopedKey[T]): Option[T] = {
101 | extracted.structure.data.get(inCurrent(extracted, key), key.key)
102 | }
103 |
104 | // surely this is harder than it has to be
105 | private def extractedLabel(extracted: Extracted): String = {
106 | val ref = extracted.currentRef
107 | val structure = extracted.structure
108 | val project = Load.getProject(structure.units, ref.build, ref.project)
109 | Keys.name in ref get structure.data getOrElse ref.project
110 | }
111 |
112 | private def collectIfMissing(extracted: Extracted, settings: Seq[Setting[_]], toCollect: Setting[_]): Seq[Setting[_]] = {
113 | val maybeExisting = getOpt(extracted, toCollect.key)
114 | maybeExisting match {
115 | case Some(x) => settings
116 | case None => settings :+ toCollect
117 | }
118 | }
119 |
120 | private def resolveStartScriptSetting(extracted: Extracted, log: Logger): Seq[Setting[_]] = {
121 | val maybePackageWar = getOpt(extracted, (packageWar in Compile).scopedKey)
122 | val maybeExportJars = getOpt(extracted, (exportJars in Compile).scopedKey)
123 |
124 | if (maybePackageWar.isDefined) {
125 | log.info("Aliasing start-script to start-script-for-war in " + extractedLabel(extracted))
126 | startScriptForWarSettings
127 | } else if (maybeExportJars.isDefined && maybeExportJars.get) {
128 | log.info("Aliasing start-script to start-script-for-jar in " + extractedLabel(extracted))
129 | startScriptForJarSettings
130 | } else if (true /* can't figure out how to decide this ("is there a main class?") without compiling first */ ) {
131 | log.info("Aliasing start-script to start-script-for-classes in " + extractedLabel(extracted))
132 | startScriptForClassesSettings
133 | } else {
134 | log.info("Aliasing start-script to start-script-not-defined in " + extractedLabel(extracted))
135 | genericStartScriptSettings ++ Seq(startScript in Compile <<= startScriptNotDefined in Compile)
136 | }
137 | }
138 |
139 | private def makeAppendSettings(settings: Seq[Setting[_]], inProject: ProjectRef, extracted: Extracted) = {
140 | // transforms This scopes in 'settings' to be the desired project
141 | val appendSettings = Load.transformSettings(Load.projectScope(inProject), inProject.build, extracted.rootProject, settings)
142 | appendSettings
143 | }
144 |
145 | private def reloadWithAppended(state: State, appendSettings: Seq[Setting[_]]): State = {
146 | val session = Project.session(state)
147 | val structure = Project.structure(state)
148 | implicit val display = Project.showContextKey(state)
149 |
150 | // reloads with appended settings
151 | val newStructure = Load.reapply(session.original ++ appendSettings, structure)
152 |
153 | // updates various aspects of State based on the new settings
154 | // and returns the updated State
155 | Project.setProject(session, newStructure, state)
156 | }
157 |
158 | private def getStartScriptTaskSettings(state: State, ref: ProjectRef): Seq[Setting[_]] = {
159 | implicit val display = Project.showContextKey(state)
160 | val extracted = Extracted(Project.structure(state), Project.session(state), ref)
161 |
162 | state.log.debug("Analyzing startScript tasks for " + extractedLabel(extracted))
163 |
164 | val resolved = resolveStartScriptSetting(extracted, state.log)
165 |
166 | var settingsToAdd = Seq[Setting[_]]()
167 | for (s <- resolved) {
168 | settingsToAdd = collectIfMissing(extracted, settingsToAdd, s)
169 | }
170 |
171 | makeAppendSettings(settingsToAdd, ref, extracted)
172 | }
173 |
174 | // command to add the startScript tasks, avoiding overriding anything the
175 | // app already has, and intelligently selecting the right target for
176 | // the "start-script" alias
177 | lazy val addStartScriptTasksCommand =
178 | Command.command("add-start-script-tasks") { (state: State) =>
179 | val allRefs = Project.extract(state).structure.allProjectRefs
180 | val allAppendSettings = allRefs.foldLeft(Seq[Setting[_]]())({ (soFar, ref) =>
181 | soFar ++ getStartScriptTaskSettings(state, ref)
182 | })
183 | val newState = reloadWithAppended(state, allAppendSettings)
184 |
185 | //println(Project.details(Project.extract(newState).structure, false, GlobalScope, startScript.key))
186 |
187 | newState
188 | }
189 |
190 | private def directoryEqualsOrContains(d: File, f: File): Boolean = {
191 | if (d == f) {
192 | true
193 | } else {
194 | val p = f.getParentFile()
195 | if (p == null)
196 | false
197 | else
198 | directoryEqualsOrContains(d, p)
199 | }
200 | }
201 |
202 | // Because we want to still work if the project directory is built and then moved,
203 | // we change all file references pointing inside build's base directory to be relative
204 | // to the build (not the project) before placing them in the start script.
205 | // This is presumably unix-specific so we skip it if the separator char is not '/'
206 | // We never add ".." to make something relative, since we are only making relative
207 | // to basedir things that are already inside basedir. If basedir moves, we'd want
208 | // references to outside of it to be absolute, to keep working. We don't support
209 | // moving projects, just the entire build, which is generally a single git repo.
210 | private def relativizeFile(baseDirectory: File, f: File, prefix: String = ".") = {
211 | if (java.io.File.separatorChar != '/') {
212 | f
213 | } else {
214 | val baseCanonical = baseDirectory.getCanonicalFile()
215 | val fCanonical = f.getCanonicalFile()
216 | if (directoryEqualsOrContains(baseCanonical, fCanonical)) {
217 | val basePath = baseCanonical.getAbsolutePath()
218 | val fPath = fCanonical.getAbsolutePath()
219 | if (fPath.startsWith(basePath)) {
220 | new File(prefix + fPath.substring(basePath.length))
221 | } else {
222 | sys.error("Internal bug: %s contains %s but is not a prefix of it".format(basePath, fPath))
223 | }
224 | } else {
225 | // leave it as-is, don't even canonicalize
226 | f
227 | }
228 | }
229 | }
230 |
231 | private def renderTemplate(template: String, fields: Map[String, String]) = {
232 | val substRegex = """@[A-Z_]+@""".r
233 | for (m <- substRegex findAllIn template) {
234 | val withoutAts = m.substring(1, m.length - 1)
235 | if (!fields.contains(withoutAts))
236 | sys.error("Template has variable %s which is not in the substitution map %s".format(withoutAts, fields))
237 | }
238 | // this is neither fast nor very correct (since if a value contains an @@ we'd substitute
239 | // on a substitution) but it's fine for private ad hoc use where we know it doesn't
240 | // matter
241 | fields.iterator.foldLeft(template)({ (s, kv) =>
242 | val withAts = "@" + kv._1 + "@"
243 | if (!s.contains(withAts))
244 | sys.error("Template does not contain variable " + withAts)
245 | s.replace(withAts, kv._2)
246 | })
247 | }
248 |
249 | private def relativeClasspathStringTask(baseDirectory: File, cp: Classpath) = {
250 | RelativeClasspathString(cp.files map { f => relativizeFile(baseDirectory, f, "$PROJECT_DIR") } mkString ("", java.io.File.pathSeparator, ""))
251 | }
252 |
253 | // Generate shell script that calculates path to project directory from its own path.
254 | private def scriptRootDetect(baseDirectory: File, scriptFile: File, otherFile: Option[File]): String = {
255 | val baseDir = baseDirectory.getCanonicalPath
256 | val scriptDir = scriptFile.getParentFile.getCanonicalPath
257 | val pathFromScriptDirToBaseDir = if (scriptDir startsWith (baseDir + File.separator)) {
258 | val relativePath = scriptDir drop (baseDir.length + 1)
259 | var parts = relativePath split Pattern.quote(File.separator)
260 | Seq.fill(parts.length)("..").mkString(File.separator)
261 | } else {
262 | sys.error("Start script must be located inside project directory.")
263 | }
264 |
265 | val templateWindows = """set PROJECT_DIR=%~dp0\@PATH_TO_PROJECT@"""
266 | val templateLinux = """PROJECT_DIR=$(cd "${BASH_SOURCE[0]%/*}" && pwd -P)/@PATH_TO_PROJECT@"""
267 | val template: String = if (isWindows()) templateWindows else templateLinux
268 | renderTemplate(template, Map("PATH_TO_PROJECT" -> pathFromScriptDirToBaseDir))
269 | }
270 |
271 | private def mainClassSetup(maybeMainClass: Option[String]): String = {
272 | maybeMainClass match {
273 | case Some(mainClass) =>
274 | if (isWindows()) {
275 | "set MAINCLASS=" + mainClass + "\r\n"
276 | } else {
277 | "MAINCLASS=" + mainClass + "\n"
278 | }
279 | case None =>
280 | val errMsg = """This "start" script requires a main class name as the first argument, because a mainClass was not specified in SBT and not autodetected by SBT (usually means you have zero, or more than one, main classes). You could specify in your SBT build: mainClass in Compile := Some("Whatever")"""
281 | if (isWindows()) {
282 | """set MAINCLASS="%1"
283 | SHIFT
284 | if "%MAINCLASS%"=="" ( echo """" + errMsg + """" && EXIT 1)
285 |
286 | """
287 | } else {
288 | """MAINCLASS="$1"
289 | shift
290 | function die() {
291 | echo $* 1>&2
292 | exit 1
293 | }
294 | if test x"$MAINCLASS" = x; then
295 | die '""" + errMsg + """'
296 | fi
297 |
298 | """
299 | }
300 | }
301 | }
302 |
303 | private def writeScript(scriptFile: File, script: String) = {
304 | IO.write(scriptFile, script)
305 | scriptFile.setExecutable(true)
306 | }
307 |
308 | def startScriptForClassesTask(streams: TaskStreams, baseDirectory: File, scriptFile: File, cpString: RelativeClasspathString, maybeMainClass: Option[String]) = {
309 | val templateWindows = """@echo off
310 | @SCRIPT_ROOT_DETECT@
311 |
312 | @MAIN_CLASS_SETUP@
313 |
314 | java %JOPTS% -cp "@CLASSPATH@" "%MAINCLASS%" %*
315 |
316 | """
317 | val templateLinux = """#!/bin/bash
318 | @SCRIPT_ROOT_DETECT@
319 |
320 | @MAIN_CLASS_SETUP@
321 |
322 | exec java $JAVA_OPTS -cp "@CLASSPATH@" "$MAINCLASS" "$@"
323 |
324 | """
325 | val template: String = if (isWindows()) templateWindows else templateLinux
326 | val script = renderTemplate(template, Map("SCRIPT_ROOT_DETECT" -> scriptRootDetect(baseDirectory, scriptFile, None),
327 | "CLASSPATH" -> cpString.value,
328 | "MAIN_CLASS_SETUP" -> mainClassSetup(maybeMainClass)))
329 | writeScript(scriptFile, script)
330 | streams.log.info("Wrote start script for mainClass := " + maybeMainClass + " to " + scriptFile)
331 | scriptFile
332 | }
333 |
334 | // the classpath string here is dependencyClasspath which includes the exported
335 | // jar, that is not what I was expecting... anyway it works out since we want
336 | // the jar on the classpath.
337 | // We put jar on the classpath and supply a mainClass because with "java -jar"
338 | // the deps have to be bundled in the jar (classpath is ignored), and SBT does
339 | // not normally do that.
340 | def startScriptForJarTask(streams: TaskStreams, baseDirectory: File, scriptFile: File, jarFile: File, cpString: RelativeClasspathString, maybeMainClass: Option[String]) = {
341 | val templateWindows = """@echo off
342 | @SCRIPT_ROOT_DETECT@
343 |
344 | @MAIN_CLASS_SETUP@
345 |
346 | java %JOPTS% -cp "@CLASSPATH@" %MAINCLASS% %*
347 |
348 | """
349 | val templateLinux = """#!/bin/bash
350 | @SCRIPT_ROOT_DETECT@
351 |
352 | @MAIN_CLASS_SETUP@
353 |
354 | exec java $JAVA_OPTS -cp "@CLASSPATH@" "$MAINCLASS" "$@"
355 |
356 | """
357 | val template: String = if (isWindows()) templateWindows else templateLinux
358 | val relativeJarFile = relativizeFile(baseDirectory, jarFile)
359 |
360 | val script = renderTemplate(template, Map("SCRIPT_ROOT_DETECT" -> scriptRootDetect(baseDirectory, scriptFile, Some(relativeJarFile)),
361 | "CLASSPATH" -> cpString.value,
362 | "MAIN_CLASS_SETUP" -> mainClassSetup(maybeMainClass)))
363 | writeScript(scriptFile, script)
364 | streams.log.info("Wrote start script for jar " + relativeJarFile + " to " + scriptFile + " with mainClass := " + maybeMainClass)
365 | scriptFile
366 | }
367 |
368 | // FIXME implement this; it will be a little bit tricky because
369 | // we need to download and unpack the Jetty "distribution" which isn't
370 | // a normal jar dependency. Not sure if Ivy can do that, may have to just
371 | // have a configurable URL and checksum.
372 | def startScriptForWarTask(streams: TaskStreams, baseDirectory: File, scriptFile: File, warFile: File, jettyHome: File, jettyContextPath: String) = {
373 |
374 | // First we need a Jetty config to move us to the right context path
375 | val contextFile = jettyHome / "contexts" / "start-script.xml"
376 |
377 | // (I guess this could use Scala's XML support, feel free to clean up)
378 | val contextFileTemplate = """
379 |