В этом уроке вы узнаете о SBT! Отдельные разделы включают:
SBT – это современный инструмент для сборки приложений. Хотя он написан на Scala и предоставляет множество удобных возможностей Scala, но он может использоваться и как инструмент для сборки общего назначения.
java -Xmx512M -jar sbt-launch.jar "$@"
[local ~/projects]$ sbt Project does not exist, create new project? (y/N/s) y Name: sample Organization: com.twitter Version [1.0]: 1.0-SNAPSHOT Scala version [2.7.7]: 2.8.1 sbt version [0.7.4]: Getting Scala 2.7.7 ... :: retrieving :: org.scala-tools.sbt#boot-scala confs: [default] 2 artifacts copied, 0 already retrieved (9911kB/221ms) Getting org.scala-tools.sbt sbt_2.7.7 0.7.4 ... :: retrieving :: org.scala-tools.sbt#boot-app confs: [default] 15 artifacts copied, 0 already retrieved (4096kB/167ms) [success] Successfully initialized directory structure. Getting Scala 2.8.1 ... :: retrieving :: org.scala-tools.sbt#boot-scala confs: [default] 2 artifacts copied, 0 already retrieved (15118kB/386ms) [info] Building project sample 1.0-SNAPSHOT against Scala 2.8.1 [info] using sbt.DefaultProject with sbt 0.7.4 and Scala 2.7.7 >
Заметьте, что это хорошая практика начинать работу со SNAPSHOT версией вашего проекта.
Мы будем создавать простой парсер JSON для простых твитов. Добавьте
следующий код в
src/main/scala/com/twitter/sample/SimpleParser.scala
package com.twitter.sample case class SimpleParsed(id: Long, text: String) class SimpleParser { val tweetRegex = "\"id\":(.*),\"text\":\"(.*)\"".r def parse(str: String) = { tweetRegex.findFirstMatchIn(str) match { case Some(m) => { val id = str.substring(m.start(1), m.end(1)).toInt val text = str.substring(m.start(2), m.end(2)) Some(SimpleParsed(id, text)) } case _ => None } } }
Этот код уродлив и содержит много ошибок, но должен скомпилироваться.
SBT может использоваться как скрипт командной строки или как консоль для сборки. Мы будем использовать его в основном как консоль для сборки, но большинство команд могут быть использованы в качестве аргументов при передаче их в SBT, например
sbt test
Заметьте, что если команда принимает аргументы, то вы должны заключить в кавычки внутренний путь аргумента, например
sbt ‘test-only com.twitter.sample.SampleSpec’
Это странный способ.
В любом случае, начните работу с вашим кодом, запустив sbt
[local ~/projects/sbt-sample]$ sbt [info] Building project sample 1.0-SNAPSHOT against Scala 2.8.1 [info] using sbt.DefaultProject with sbt 0.7.4 and Scala 2.7.7 >
SBT позволяет вам запустить Scala REPL со всеми вашими зависимостями.
Перед запуском консоли, сначала компилируется исходный код вашего проекта,
предоставляя нам быстрый способ прогнать тест нашего парсера.
> console [info] [info] == compile == [info] Source analysis: 0 new/modified, 0 indirectly invalidated, 0 removed. [info] Compiling main sources... [info] Nothing to compile. [info] Post-analysis: 3 classes. [info] == compile == [info] [info] == copy-test-resources == [info] == copy-test-resources == [info] [info] == test-compile == [info] Source analysis: 0 new/modified, 0 indirectly invalidated, 0 removed. [info] Compiling test sources... [info] Nothing to compile. [info] Post-analysis: 0 classes. [info] == test-compile == [info] [info] == copy-resources == [info] == copy-resources == [info] [info] == console == [info] Starting scala interpreter... [info] Welcome to Scala version 2.8.1.final (Java HotSpot(TM) 64-Bit Server VM, Java 1.6.0_22). Type in expressions to have them evaluated. Type :help for more information. scala>
Наш код скомпилировался, и мы попадаем в обычный Scala REPL. Мы создадим новый парсер, тестовый твит, и убедимся, что все “работает”
scala> import com.twitter.sample._ import com.twitter.sample._ scala> val tweet = """{"id":1,"text":"foo"}""" tweet: java.lang.String = {"id":1,"text":"foo"} scala> val parser = new SimpleParser parser: com.twitter.sample.SimpleParser = com.twitter.sample.SimpleParser@71060c3e scala> parser.parse(tweet) res0: Option[com.twitter.sample.SimpleParsed] = Some(SimpleParsed(1,"foo")) scala>
Наш простой парсер работает с небольшим набором входных данных, но мы хотим добавить тесты и “разрушить” программу. Первым шагом будет добавление специальной библиотеки тестов и настоящий JSON парсер в наш проект. Чтобы сделать это, нам нужно выйти за рамки стандартного SBT проекта и создать свой.
SBT считает Scala файлы в директории project/build, как файлы описания проекта. Добавьте следующее в файл project/build/SampleProject.scala
import sbt._ class SampleProject(info: ProjectInfo) extends DefaultProject(info) { val jackson = "org.codehaus.jackson" % "jackson-core-asl" % "1.6.1" val specs = "org.scala-tools.testing" % "specs_2.8.0" % "1.6.5" % "test" }
Описание проекта – это SBT класс. В нашем случае, мы расширяем стандартный DefaultProject SBT.
Вы объявляете зависимости с помощью val. SBT использует отражение, чтобы найти все val зависимости в вашем проекте и построить дерево зависимостей во время сборки. Синтаксис может показаться новым, но он эквивалентен тому, что используется в Maven
<dependency> <groupId>org.codehaus.jackson</groupId> <artifactId>jackson-core-asl</artifactId> <version>1.6.1</version> </dependency> <dependency> <groupId>org.scala-tools.testing</groupId> <artifactId>specs_2.8.0</artifactId> <version>1.6.5</version> <scope>test</scope> </dependency>
Теперь мы можем получить все зависимости вашего проекта. В командной строке (не в sbt консоли), наберите sbt update
[local ~/projects/sbt-sample]$ sbt update [info] Building project sample 1.0-SNAPSHOT against Scala 2.8.1 [info] using SampleProject with sbt 0.7.4 and Scala 2.7.7 [info] [info] == update == [info] :: retrieving :: com.twitter#sample_2.8.1 [sync] [info] confs: [compile, runtime, test, provided, system, optional, sources, javadoc] [info] 1 artifacts copied, 0 already retrieved (2785kB/71ms) [info] == update == [success] Successful. [info] [info] Total time: 1 s, completed Nov 24, 2010 8:47:26 AM [info] [info] Total session time: 2 s, completed Nov 24, 2010 8:47:26 AM [success] Build completed successfully.
Вы увидете, что sbt загрузит все необходимые библиотеки. Теперь у вас появилась директория lib_managed, а lib_managed/scala_2.8.1/test будет содержать specs_2.8.0-1.6.5.jar
Теперь, когда была добавлена тестовая библиотека, добавьте следующий код в
src/test/scala/com/twitter/sample/SimpleParserSpec.scala
package com.twitter.sample import org.specs._ object SimpleParserSpec extends Specification { "SimpleParser" should { val parser = new SimpleParser() "work with basic tweet" in { val tweet = """{"id":1,"text":"foo"}""" parser.parse(tweet) match { case Some(parsed) => { parsed.text must be_==("foo") parsed.id must be_==(1) } case _ => fail("didn't parse tweet") } } } }
Запустите тест, набрав в sbt консоли
> test [info] [info] == compile == [info] Source analysis: 0 new/modified, 0 indirectly invalidated, 0 removed. [info] Compiling main sources... [info] Nothing to compile. [info] Post-analysis: 3 classes. [info] == compile == [info] [info] == test-compile == [info] Source analysis: 0 new/modified, 0 indirectly invalidated, 0 removed. [info] Compiling test sources... [info] Nothing to compile. [info] Post-analysis: 10 classes. [info] == test-compile == [info] [info] == copy-test-resources == [info] == copy-test-resources == [info] [info] == copy-resources == [info] == copy-resources == [info] [info] == test-start == [info] == test-start == [info] [info] == com.twitter.sample.SimpleParserSpec == [info] SimpleParserSpec [info] SimpleParser should [info] + work with basic tweet [info] == com.twitter.sample.SimpleParserSpec == [info] [info] == test-complete == [info] == test-complete == [info] [info] == test-finish == [info] Passed: : Total 1, Failed 0, Errors 0, Passed 1, Skipped 0 [info] [info] All tests PASSED. [info] == test-finish == [info] [info] == test-cleanup == [info] == test-cleanup == [info] [info] == test == [info] == test == [success] Successful. [info] [info] Total time: 0 s, completed Nov 24, 2010 8:54:45 AM >
Наш тест работает! Теперь можем добавить еще кода. Одной из прекрасных вещей SBT является возможностть запускать условые действия. До запуска какого-то действия, стартует цикл, который запускает событие, как только происходит изменение в исходниках. Давайте запустим ~test и посмотрим, что произойдет.
[info] == test == [success] Successful. [info] [info] Total time: 0 s, completed Nov 24, 2010 8:55:50 AM 1. Waiting for source changes... (press enter to interrupt)
Теперь давайте добавим следующий набор тестов
"reject a non-JSON tweet" in { val tweet = """"id":1,"text":"foo"""" parser.parse(tweet) match { case Some(parsed) => fail("didn't reject a non-JSON tweet") case e => e must be_==(None) } } "ignore nested content" in { val tweet = """{"id":1,"text":"foo","nested":{"id":2}}""" parser.parse(tweet) match { case Some(parsed) => { parsed.text must be_==("foo") parsed.id must be_==(1) } case _ => fail("didn't parse tweet") } } "fail on partial content" in { val tweet = """{"id":1}""" parser.parse(tweet) match { case Some(parsed) => fail("didn't reject a partial tweet") case e => e must be_==(None) } }
После того как мы сохраним наш файл, SBT определит наши изменения, запустит тесты и проинформирует нас, если парсер работает некорректно
[info] == com.twitter.sample.SimpleParserSpec == [info] SimpleParserSpec [info] SimpleParser should [info] + work with basic tweet [info] x reject a non-JSON tweet [info] didn't reject a non-JSON tweet (Specification.scala:43) [info] x ignore nested content [info] 'foo","nested":{"id' is not equal to 'foo' (SimpleParserSpec.scala:31) [info] + fail on partial content
Теперь давайте перепишем наш JSON парсер, чтобы он больше соответствовал действительности
package com.twitter.sample import org.codehaus.jackson._ import org.codehaus.jackson.JsonToken._ case class SimpleParsed(id: Long, text: String) class SimpleParser { val parserFactory = new JsonFactory() def parse(str: String) = { val parser = parserFactory.createJsonParser(str) if (parser.nextToken() == START_OBJECT) { var token = parser.nextToken() var textOpt:Option[String] = None var idOpt:Option[Long] = None while(token != null) { if (token == FIELD_NAME) { parser.getCurrentName() match { case "text" => { parser.nextToken() textOpt = Some(parser.getText()) } case "id" => { parser.nextToken() idOpt = Some(parser.getLongValue()) } case _ => // noop } } token = parser.nextToken() } if (textOpt.isDefined && idOpt.isDefined) { Some(SimpleParsed(idOpt.get, textOpt.get)) } else { None } } else { None } } }
Получили простой JSON парсер. Когда мы сохраним изменения, SBT перекомпилирует наш код и запустит наши тесты. Получилось намного лучше!
info] SimpleParser should [info] + work with basic tweet [info] + reject a non-JSON tweet [info] x ignore nested content [info] '2' is not equal to '1' (SimpleParserSpec.scala:32) [info] + fail on partial content [info] == com.twitter.sample.SimpleParserSpec ==
Нам нужно проверить наши вложенные объекты. Давайте добавим немного ужасных
условий в наш цикл чтения.
def parse(str: String) = { val parser = parserFactory.createJsonParser(str) var nested = 0 if (parser.nextToken() == START_OBJECT) { var token = parser.nextToken() var textOpt:Option[String] = None var idOpt:Option[Long] = None while(token != null) { if (token == FIELD_NAME && nested == 0) { parser.getCurrentName() match { case "text" => { parser.nextToken() textOpt = Some(parser.getText()) } case "id" => { parser.nextToken() idOpt = Some(parser.getLongValue()) } case _ => // noop } } else if (token == START_OBJECT) { nested += 1 } else if (token == END_OBJECT) { nested -= 1 } token = parser.nextToken() } if (textOpt.isDefined && idOpt.isDefined) { Some(SimpleParsed(idOpt.get, textOpt.get)) } else { None } } else { None } }
И… все работает!
С этого момента мы можем запустить команду создания пакета, чтобы сгенерировать jar файл. Тем не менее, нам может быть захочется поделиться нашим jar файлом с другими командами. Делать это мы будем с помощью StandardProject, который дает нам большое преимущество.
Первым шагом будет включение StandardProject как SBT плагина. Плагины – это способ добавить зависимость в вашу сборку, а не в проект. Эти зависимости располагаются в project/plugins/Plugins.scala. Добавьте следующие строки в файл Plugins.scala.
import sbt._ class Plugins(info: ProjectInfo) extends PluginDefinition(info) { val twitterMaven = "twitter.com" at "http://maven.twttr.com/" val defaultProject = "com.twitter" % "standard-project" % "0.7.14" }
Заметьте, что мы определяем специальный maven репозиторий, также как и зависимости. Это происходит потому, что стандартная библиотека проекта не является стандартным sbt репозиторием.
Мы также обновим наше описание проекта, расширив StandardProject, включая ветку в SVN, и определим репозиторий для публикации. Измените SampleProject.scala на следующее
import sbt._ import com.twitter.sbt._ class SampleProject(info: ProjectInfo) extends StandardProject(info) with SubversionPublisher { val jackson = "org.codehaus.jackson" % "jackson-core-asl" % "1.6.1" val specs = "org.scala-tools.testing" % "specs_2.8.0" % "1.6.5" % "test" override def subversionRepository = Some("http://svn.local.twitter.com/maven/") }
Теперь, если мы попробуем опубликовать сборку мы получим следующее
[info] == deliver == IvySvn Build-Version: null IvySvn Build-DateTime: null [info] :: delivering :: com.twitter#sample;1.0-SNAPSHOT :: 1.0-SNAPSHOT :: release :: Wed Nov 24 10:26:45 PST 2010 [info] delivering ivy file to /Users/mmcbride/projects/sbt-sample/target/ivy-1.0-SNAPSHOT.xml [info] == deliver == [info] [info] == make-pom == [info] Wrote /Users/mmcbride/projects/sbt-sample/target/sample-1.0-SNAPSHOT.pom [info] == make-pom == [info] [info] == publish == [info] :: publishing :: com.twitter#sample [info] Scheduling publish to http://svn.local.twitter.com/maven/com/twitter/sample/1.0-SNAPSHOT/sample-1.0-SNAPSHOT.jar [info] published sample to com/twitter/sample/1.0-SNAPSHOT/sample-1.0-SNAPSHOT.jar [info] Scheduling publish to http://svn.local.twitter.com/maven/com/twitter/sample/1.0-SNAPSHOT/sample-1.0-SNAPSHOT.pom [info] published sample to com/twitter/sample/1.0-SNAPSHOT/sample-1.0-SNAPSHOT.pom [info] Scheduling publish to http://svn.local.twitter.com/maven/com/twitter/sample/1.0-SNAPSHOT/ivy-1.0-SNAPSHOT.xml [info] published ivy to com/twitter/sample/1.0-SNAPSHOT/ivy-1.0-SNAPSHOT.xml [info] Binary diff deleting com/twitter/sample/1.0-SNAPSHOT [info] Commit finished r977 by 'mmcbride' at Wed Nov 24 10:26:47 PST 2010 [info] Copying from com/twitter/sample/.upload to com/twitter/sample/1.0-SNAPSHOT [info] Binary diff finished : r978 by 'mmcbride' at Wed Nov 24 10:26:47 PST 2010 [info] == publish == [success] Successful. [info] [info] Total time: 4 s, completed Nov 24, 2010 10:26:47 AM
и (спустя некоторое время), если мы зайдем на binaries.local.twitter.com:http://binaries.local.twitter.com/maven/com/twitter/sample/1.0-SNAPSHOT/ , то увидем наш опубликованный jar файл.
Задачи – это Scala функции. Простейший способ добавить задачу, нужно влючить val в ваше описание проекта, используя метод task, например
lazy val print = task {log.info("a test action"); None}
Если вам нужны зависимости и описание, то их вы можете добавить так
lazy val print = task {log.info("a test action"); None}.dependsOn(compile) describedAs("prints a line after compile")
Если мы перезагрузим наш проект и наберем команду print мы увидим следующее
> print [info] [info] == print == [info] a test action [info] == print == [success] Successful. [info] [info] Total time: 0 s, completed Nov 24, 2010 11:05:12 AM >
Итак, все работает. Если объявляете задачу в одном проекте, то все работает прекрасно. Но если вы определяете задачу как плагин, то это будет непрактично. Я хочу, чтобы было так
lazy val print = printAction def printAction = printTask.dependsOn(compile) describedAs("prints a line after compile") def printTask = task {log.info("a test action"); None}
Это позволяет пользователям переопределить саму задачу, зависимости и/или описание задачи или действия. Большинство SBT проектов следуют этому образцу. В качестве примера, мы можем изменить встроенные задачи пакета для печати текущего времени, выполните следующие действия
lazy val printTimestamp = task { log.info("current time is " + System.currentTimeMillis); None} override def packageAction = super.packageAction.dependsOn(printTimestamp)
Есть много примеров в StandardProject по настройке стандартных значений SBT и добавлению пользовательских задач.
TBD