├── .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 | 380 | @CONTEXTPATH@ 381 | /webapps/@WARFILE_BASENAME@ 382 | 383 | """ 384 | val contextFileContents = renderTemplate(contextFileTemplate, 385 | Map("WARFILE_BASENAME" -> warFile.getName, 386 | "CONTEXTPATH" -> jettyContextPath)) 387 | IO.write(contextFile, contextFileContents) 388 | 389 | val templateWindows = """ 390 | @echo off 391 | @SCRIPT_ROOT_DETECT@ 392 | 393 | copy "@WARFILE@" "@JETTY_HOME@\webapps" || (echo "Failed to copy @WARFILE@ to @JETTY_HOME@\webapps" && EXIT 1) 394 | 395 | if "%PORT%"=="" (set PORT=8080) 396 | 397 | java %JAVA_OPTS% -Djetty.port="%PORT%" -Djetty.home="@JETTY_HOME@" -jar "@JETTY_HOME@\start.jar" %* 398 | 399 | """ 400 | val templateLinux = """#!/bin/bash 401 | @SCRIPT_ROOT_DETECT@ 402 | 403 | /bin/cp -f "@WARFILE@" "@JETTY_HOME@/webapps" || die "Failed to copy @WARFILE@ to @JETTY_HOME@/webapps" 404 | 405 | if test x"$PORT" = x ; then 406 | PORT=8080 407 | fi 408 | 409 | exec java $JAVA_OPTS -Djetty.port="$PORT" -Djetty.home="@JETTY_HOME@" -jar "@JETTY_HOME@/start.jar" "$@" 410 | 411 | """ 412 | val template: String = if (isWindows()) templateWindows else templateLinux 413 | val relativeWarFile = relativizeFile(baseDirectory, warFile) 414 | 415 | val script = renderTemplate(template, 416 | Map("SCRIPT_ROOT_DETECT" -> scriptRootDetect(baseDirectory, scriptFile, Some(relativeWarFile)), 417 | "WARFILE" -> relativeWarFile.toString, 418 | "JETTY_HOME" -> jettyHome.toString)) 419 | writeScript(scriptFile, script) 420 | 421 | streams.log.info("Wrote start script for war " + relativeWarFile + " to " + scriptFile) 422 | scriptFile 423 | } 424 | 425 | // this is weird; but I can't figure out how to have a "startScript" task in the root 426 | // project that chains to child tasks, without having this dummy. For example "package" 427 | // works the same way, it even creates a bogus empty jar file in the root project! 428 | def startScriptNotDefinedTask(streams: TaskStreams, scriptFile: File) = { 429 | val errMsg = "No meaningful way to start this project was defined in the SBT build" 430 | val msgWindows = """ 431 | echo """" + errMsg + """" 1>&2 432 | EXIT 1 433 | """ 434 | val msgLinux = """#!/bin/bash 435 | echo '""" + errMsg + """' 1>&2 436 | exit 1 437 | """ 438 | val msg: String = if (isWindows()) msgWindows else msgLinux 439 | writeScript(scriptFile, msg) 440 | streams.log.info("Wrote start script that always fails to " + scriptFile) 441 | scriptFile 442 | } 443 | 444 | private def basenameFromURL(url: URL) = { 445 | val path = url.getPath 446 | val slash = path.lastIndexOf('/') 447 | if (slash < 0) 448 | path 449 | else 450 | path.substring(slash + 1) 451 | } 452 | 453 | def startScriptJettyHomeTask(streams: TaskStreams, target: File, jettyURLString: String, jettyChecksum: String) = { 454 | try { 455 | val jettyURL = new URL(jettyURLString) 456 | val jettyDistBasename = basenameFromURL(jettyURL) 457 | if (!jettyDistBasename.endsWith(".zip")) 458 | sys.error("%s doesn't end with .zip".format(jettyDistBasename)) 459 | val jettyHome = target / jettyDistBasename.substring(0, jettyDistBasename.length - ".zip".length) 460 | 461 | val zipFile = target / jettyDistBasename 462 | if (!zipFile.exists()) { 463 | streams.log.info("Downloading %s to %s".format(jettyURL.toExternalForm, zipFile)) 464 | IO.download(jettyURL, zipFile) 465 | } else { 466 | streams.log.debug("%s already exists".format(zipFile)) 467 | } 468 | val sha1 = Hash.toHex(Hash(zipFile)) 469 | if (sha1 != jettyChecksum) { 470 | streams.log.error("%s has checksum %s expected %s".format(jettyURL.toExternalForm, sha1, jettyChecksum)) 471 | sys.error("Bad checksum on Jetty distribution") 472 | } 473 | try { 474 | IO.delete(jettyHome) 475 | } catch { 476 | case e: Exception => // probably didn't exist 477 | } 478 | val files = IO.unzip(zipFile, target) 479 | val jettyHomePrefix = jettyHome.getCanonicalPath 480 | // check that all the unzipped files went where expected 481 | files foreach { f => 482 | if (!f.getCanonicalPath.startsWith(jettyHomePrefix)) 483 | sys.error("Unzipped jetty file %s that isn't in %s".format(f, jettyHome)) 484 | } 485 | streams.log.debug("Unzipped %d files to %s".format(files.size, jettyHome)) 486 | 487 | // delete annoying test.war and associated gunge 488 | for (deleteContentsOf <- (Seq("contexts", "webapps") map { jettyHome / _ })) { 489 | val contents = PathFinder(deleteContentsOf) ** new SimpleFileFilter({ f => 490 | f != deleteContentsOf 491 | }) 492 | for (doNotWant <- contents.get) { 493 | streams.log.debug("Deleting test noise " + doNotWant) 494 | IO.delete(doNotWant) 495 | } 496 | } 497 | 498 | jettyHome 499 | } catch { 500 | case e: Exception => 501 | streams.log.error("Failure obtaining Jetty distribution: " + e.getMessage) 502 | throw e 503 | } 504 | } 505 | 506 | def stageTask(startScriptFile: File) = { 507 | // we don't do anything for now 508 | } 509 | } 510 | -------------------------------------------------------------------------------- /src/sbt-test/start/00basic/build.sbt: -------------------------------------------------------------------------------- 1 | import com.typesafe.sbt.SbtStartScript 2 | 3 | seq(SbtStartScript.startScriptForClassesSettings: _*) 4 | 5 | version := "0.1" 6 | 7 | TaskKey[Unit]("check") <<= (target) map { (target) => 8 | val process = sbt.Process((target / "start").toString) 9 | val out = (process!!) 10 | if (out.trim != "Hello") error("unexpected output: " + out) 11 | () 12 | } 13 | -------------------------------------------------------------------------------- /src/sbt-test/start/00basic/project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("com.typesafe.sbt" % "sbt-start-script" % "0.9.0-SNAPSHOT") 2 | -------------------------------------------------------------------------------- /src/sbt-test/start/00basic/src/main/scala/Hello.scala: -------------------------------------------------------------------------------- 1 | object Main extends App { 2 | println("Hello") 3 | } 4 | -------------------------------------------------------------------------------- /src/sbt-test/start/00basic/test: -------------------------------------------------------------------------------- 1 | > start-script 2 | $ exists target/start 3 | > check 4 | 5 | -------------------------------------------------------------------------------- /src/sbt-test/start/01jar/build.sbt: -------------------------------------------------------------------------------- 1 | import com.typesafe.sbt.SbtStartScript 2 | 3 | seq(SbtStartScript.startScriptForJarSettings: _*) 4 | 5 | version := "0.1" 6 | 7 | TaskKey[Unit]("check") <<= (target) map { (target) => 8 | val process = sbt.Process((target / "start").toString) 9 | val out = (process!!) 10 | if (out.trim != "Hello") error("unexpected output: " + out) 11 | () 12 | } 13 | -------------------------------------------------------------------------------- /src/sbt-test/start/01jar/project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("com.typesafe.sbt" % "sbt-start-script" % "0.9.0-SNAPSHOT") 2 | -------------------------------------------------------------------------------- /src/sbt-test/start/01jar/src/main/scala/Hello.scala: -------------------------------------------------------------------------------- 1 | object Main extends App { 2 | println("Hello") 3 | } 4 | -------------------------------------------------------------------------------- /src/sbt-test/start/01jar/test: -------------------------------------------------------------------------------- 1 | > start-script 2 | $ exists target/start 3 | > check 4 | 5 | -------------------------------------------------------------------------------- /src/sbt-test/start/02war/build.sbt: -------------------------------------------------------------------------------- 1 | import com.typesafe.sbt.SbtStartScript 2 | import com.earldouglas.xsbtwebplugin.WebPlugin 3 | 4 | seq(SbtStartScript.startScriptForWarSettings: _*) 5 | 6 | seq(WebPlugin.webSettings: _*) 7 | 8 | version := "0.1" 9 | 10 | libraryDependencies ++= Seq("javax.servlet" % "servlet-api" % "2.5" % "provided", 11 | "org.eclipse.jetty" % "jetty-webapp" % "7.3.1.v20110307" % "container") 12 | -------------------------------------------------------------------------------- /src/sbt-test/start/02war/project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("com.typesafe.sbt" % "sbt-start-script" % "0.9.0-SNAPSHOT") 2 | 3 | addSbtPlugin("com.earldouglas" % "xsbt-web-plugin" % "0.4.2") 4 | -------------------------------------------------------------------------------- /src/sbt-test/start/02war/src/main/scala/Hello.scala: -------------------------------------------------------------------------------- 1 | object Main extends App { 2 | println("Hello") 3 | } 4 | 5 | package test { 6 | 7 | import javax.servlet.http._ 8 | 9 | class MyServlet extends HttpServlet { 10 | val html = 11 | MyServlet 12 | Hello 13 | 14 | 15 | override def doGet(request: HttpServletRequest, response: HttpServletResponse) { 16 | response.setContentType("text/html") 17 | response.getWriter().print(html.toString) 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/sbt-test/start/02war/src/main/webapp/WEB-INF/web.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | Test App 7 | 8 | 9 | MyServlet 10 | test.MyServlet 11 | 12 | 13 | MyServlet 14 | / 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/sbt-test/start/02war/test: -------------------------------------------------------------------------------- 1 | > start-script 2 | $ exists target/start 3 | 4 | # FIXME we should run the web server and test but will 5 | # take some figuring out how to run target/start and 6 | # then http GET 7 | --------------------------------------------------------------------------------