Sbt #1 : Temeller


 Sbt, scala icin var olan en populer build system'lerden bir tanesidir. Peki neden bir build system kullanmaliyiz?

1. compile
2. dependency management
3. test
4. package

Bu asamalarin hepsini manuel olarak yapmak heralde cok sacma olacaktir. 

Sbt, 2008'de ortaya cikiyor.  Ilk ciktiginda ismi simple build tool seklinde olsa da sonra scala build tool olarak degistiriliyor. Ancak gunumuzde java ve scala projeleri icin kullanilabildiginden artik o anlamini da yitirdi ve sbt bir anlama gelmiyor. Java build tool'lari olan Maven ve Ant ile ayni isi yapiyor denilebilir. Gelistirilmesi ise komunite ve Lightbend (scala ve ekosisteminin arkasinda olan sirket) tarafindan gerceklestiriliyor ki arkasi saglam.

Sbt'nin baslica ozellikleri:

1. Zero config: cok az bir konfigurasyon gerektirmesi
2. Testing: scala-test, junit, scala-check ve specs testing frameworkleri ile iyi integrasyon saglar
3. Incremental compilation: proje ve testler icin incremental compilation saglar
4. Multi-projects: birden fazla projeyi ya da (sub-project seklinde) yontebilir.
5. Parallel task execution: testleri paralel olarak calistirabilir
6. Library config management: Maven ya da Ivy library managerlarini kullanabilir. 

Kurulum
Kendi sitesindeki download linkinden kolayca kurulabilir. Sadece oncesinde jdk8 kurulumuna sahip olmaniz gerekmektedir. 

Ayrica bu hikayede isleyecegimiz ornekleri tamamlayabilmek icin IntelliJ de kurmaniz gerekiyor. Community edition tamamen ucretsiz ve scala plugin de kurulbiliyor. 

IntelliJ'i kurduktan sonra New Project deyip, gelen wizardda sbt seciyoruz. Daha sonra sbt-shell panelindeyken sbtVersion yazarak kurulumu dogruluyoruz. Su sekilde bir sonuc almaniz gerekli:

[IJ]sbtVersion
[info] 1.4.6

Sbt 101
Sbt'de hersey ana proje kalsoru uzerinden yururu. Bu acidan bir adet klasor olusturuyoruz. 

> mkdir sbt101
> cd sbt101
> sbt

Bu klasor icerisinde sbt'yi direk calistiralim. Sonuc, bize minimum gerekli olan build.sbt dosyasinin mevcut olmadigini hatirlatan bir mesaj.

[warn] Neither build.sbt nor a 'project' directory in the current directory: 
c) continue
q) quit
?

Quit ile ciktiktan sonra, bos bir build.sbt dosyasi olsuturalim. Boylece anlamis olduk ki bir sbt projesi icin gereken en az sey bos da olsa bir build.sbt dosyasidir. 

> touch build.sbt
> sbt
[info] Updated file /sbt101/project/build.properties: set sbt.version to 1.3.4 
[info] Loading project definition from /sbt101/project
[info] Loading settings for project sbt101 from build.sbt ...
[info] Set current project to sbt101 (in build file:/sbt101/)
[info] sbt server started at local:///home/user/.sbt/1.0/server/1c36283f496f4ead6f43/sock
sbt:sbt101>

1. Sbt, project adinda bir directory olusturuyor ve icerisinde build.properties isimli bir dosya olusturuyor. Bu dosya, bizim mevcut sbt versiyonumuzu (1.3.4) tutuyor. Bu da bizim projemizin bu sbt versiyonu ile calisacagini belirtiyor. Bu sayede bu projeyi baska birisi alip build etmek istese, kendileri farkli bir sbt versiyonuna sahip bile olsa, sbt bu versiyonu indirecek ve bizim proje icin bu versiyonu kullanacaktir. Bu sayede de version uyumsuzluklarinin onune gecilir. 

2. project klasoru okunarak gerekli scala dosyalari varsa bunlarin okunmasi saglanir. 

3. build.sbt okunarak build icin gerekli tanimlamalar alinir. Simdilik bur dosya bos oldugu icin birsey okunamadi.

4. Set current project to sbt101 satirinda gosteriyor ki projemizin adi sbt101. build.sbt'de proje ismi vermedigimiz icin bu klasor adindan alindi. 

5. son olarak da interaktif sbt shell calismaya basliyor. Bu noktada  help yazarak komutlar hakkinda bilgi alabiliriz.

Compile
Proje klasoru bos oldugu icin simdilik fazla bisey yapamiyoruz. Gelin bir source dosyasi olusturalim. 

> mkdir sbt101
> cd sbt101
> mkdir -p scr/main/scala
> vim src/main/scala/HelloWorld.scala

object HelloWorld extends App {
    println("Hello world!")
}

App'tan tureyen bir object olusturuyoruz ki bu onu bir executable yapiyor. Kaydedip, sbt shell'i acip, compile ediyoruz.

> sbt
sbt:sbt101> compile
sbt:sbt101> compile
[info] Compiling 1 Scala source to /sbt101/target/scala-2.12/classes ...
[success] Total time: 1 s, completed Jan 7, 2021, 7:13:26 AM

Dikkat edilirse, sbt bizim olusturdugumuz 1 scala dosyasini gordu ve onu compile etti. Bu noktada uygulamamizi calistirmak icin tek yapmamiz gereken run komutunu calistirmak.

sbt:sbt102> run
[info] running HelloWorld
Hello World!
[success] Total time: 1 s, completed Jan 7, 2021, 7:13:54 AM

Bu klasigi de tekrar birlikte yasamis olduk sayin okurlar :D Peki ya birden fazla executable class'imiz olursa? O zaman birtane de Echo.scala olusturalim:

> vim src/main/Echo.scala

object Echo extends App {
    println(s"Echo diyor ki: ${args.mkString(" ")}")
}

Tekrar sbt shell'e gecip argumanlar ile calistiriyoruz:

>sbt
sbt:sbt101> run A B C
[warn] Multiple main classes detected.  Run 'show discoveredMainClasses' to see the list
Multiple main classes detected, select one to run:

 [1] Echo
 [2] HelloWorld

[info] running Echo A B C
diyor ki: A B C
[success] Total time: 2 s, completed Jan 7, 2021, 7:25:28 AM

Ozet olarak direk run komutu calistirdigimizda eger birden fazla executable class var ise evcut projede, sbt bize bir liste cikartarak hangisini kastettigimizi soruyor. Burada 1 'e basarak Echo'yu calistirdim ve sonuc ortada.

Birden fazla komut 
Birden fazla komutu ayni anda calistirmak icin komut baslarina noktali virgul koyuyoruz ve komutlarin arasina da birer bosluk koyuyoruz. 

sbt:sbt101> ;clean ;compile

History
Daha once calistirilan komutlara ulasmak icin faydali komutlardir. Unlem yazip entera basarak daha fazla bilgi alabiliriz. 

sbt:sbt101> !

History commands:
  !!    Execute the last command again
  !:    Show all previous commands
  !:n    Show the last n commands
  !n    Execute the command with index n, as shown by the !: command
  !-n    Execute the nth command before this one
  !string    Execute the most recent command starting with 'string'
  !?string    Execute the most recent command containing 'string'

Yani ornek olarak enson ;clean ;compile calistirdi isek, !! yaparak tekrar son calisan komutu calistirabiliriz. 

Projec Ici Scala Repl
Bir poje uzerinde calisiyoruz ve bir feature icin cesitli denemeler yapmamiz gerekiyor. Bu durumda interactive bir scala shell ise yaracaktir. Ancak, projedeki diger class'lara da ulasabiliyor olmamiz gerek. Bu durumda console yazarak scala console'a gecebiliriz. 

sbt:sbt101> console
[info] Starting scala interpreter...
Welcome to Scala 2.12.10 (OpenJDK 64-Bit Server VM, Java 11.0.8).
Type in expressions for evaluation. Or try :help.

scala> Echo.main(Array("kim", "neyi", "nereden", "ne belli"))
diyor ki: kim neyi nereden ne belli

Bu sekilde scala repl'a ulastik ve de projemizdeki Echo class'ini direk olarak kullanabiliyoruz. Ayni ozelligi bir Zeppelin notebook'ta ya da Jupyter eklentileriyle elde etmek icin projemizi derleyip jar'i import etmek gerekir ki zahmetli isler bunlar. 

Isimiz bitince de :q ile tekrar sbt shell'e gecebiliriz.  Sbt shell'e de exit yazarak cikis yapiyoruz.

Sbt ile calismanin iki yolu vardir. Birincisini hep birlikte gorduk, sbt shell. Digeri ise sbt'ye arguman vererek komut satirindan calstirmak. 

> sbt clean compile

Seklinde iki komutu arka arkaya calistiriyoruz ve hic sbt shell'e girmiyoruz. Burada dikkat edilmesi gereken bir husus ise, bu sekilde her calistirdigimizda yeni bir jvm olusturuluyor ve bu da bizi yavaslatiyor. Ancak sbt shell kullanildiginda sadece tek bir jvm olusturuluyor ve tum komutlar bunun uzerinde calisiyor. Bu acidan daha yuksek bir performans elde ediliyor. Yine de bash modda class'a arguman gondermek istersek, bunu cift tirnak icine alarak yapabiliyoruz:

> sbt "run ne kimi neyi ne belli"

Klasor Yapisi
Sbt'de hersey bir root directory ile baslar demistik. Simdi bu directory'nin icerigine goz atalim.

sbt101                                                baseDirectory
    src/                                                 sourceDirectories
        main/
            scala/
            java/
            resources/                               resourceDirectories

    test/                                                test:sourceDirectories
        scala/
        java/
        resources/                                   test:resourceDirectories

    build.sbt                                         build definition

    project/                                           build destek dosyalari
        build.properties
        # daha fazla *.scala 

    target/

sourceDirectories: Proje kaynak kod dosyalarini barindirir. Bu klasor disindaki kaynak kod dosyalari ignore edilir. Ayrica tum hidden dosyalar da sbt tarafindan ignore edilir. 

resourceDirectories: Burada diger yardimci dosyalar bulunur. 

build.sbt: Build icin gerekli olan tum konfig bu dosyada bulunur

target: sbt tarafindan olustulan class ve jar dosyalari gibi artifactlari barindirir

Simdi buradaki klasorleri yanda verilen sbt isimleri ile (ornegin resourceDirectories), sbt shell'de sorgulayabiliriz. 

sbt:sbt101> sourceDirectories
[info] * /sbt101/src/main/scala-2.12
[info] * /sbt101/src/main/scala
[info] * /sbt101/src/main/java
[info] * /sbt101/target/scala-2.12/src_managed/main

Bu arada elbette ki sbt shell'de tab kullanarak secenekleri gormek mumkun (tab based completion).

Build.sbt ile build tanimlamasi
Simdi bos olan build.sbt dosyasina proje adini ekleyelim.

name := "sbt201"

ve daha sonra sbt shell'de iken proje adini check edelim:

sbt:sbt101> name
[warn] build source files have changed
[warn] modified files:
[warn]   /sbt101/build.sbt
[warn] Apply these changes by running `reload`.
[warn] Automatically reload the build when source changes are detected by setting `Global / onChangedBuildSource := ReloadOnSourceChanges`.
[warn] Disable this warning by setting `Global / onChangedBuildSource := IgnoreSourceChanges`.
[info] sbt101

 Cok kibar bir dille uyariliyoruz ki, build.sbt dosyasi degismis ama biz reload komutu ile onu tekrar yuklememisiz. Bir sbt projesinde build.sbt dosyasi degisir ise, degisikliklerin projeye uygulanmasi icin bizim manuel olarak reload komutunu calistirmamiz gerekir. Reload edip tekrar name komutunu calistirirsak istedigimiz sonucu elde etmis oluyoruz:

sbt:sbt101> ;reload ;name
[info] Loading project definition from /sbt101/project
[info] Loading settings for project sbt101 from build.sbt ...
[info] Set current project to sbt201 (in build file:/sbt101/)
[info] sbt201

Peki proje adini nasil atadik? build.sbt dosyasinda hersey key-value pairleri seklinde tanimlanir. Burada key, seperator ve bir de value olmak zorunda. Ornekte key name, seperator := (atama operatoru) ve de values ise "sbt201" seklinde oldugu gorulur. Bu, setting expression denilen bir tanimlama seklidir. Sbt'de uc cesit key tanimlamasi yapilabilir:

1- SettingKey[T] : proje icin sadece 1 defa hesaplanir

2- TaskKey[T] : her cagrildiginda tekrar hesaplanir

3- InputKey[T] : komut satirindan arguman verilebilir

sbt uzerinde inspect komutu ile bir key'in tipin hakkinda bilgi edinebiliriz.

sbt:sbt201> inspect name
[info] Setting: java.lang.String = sbt201
[info] Description:
[info]  Project name.
... 

Bircok bilgi gosteriliyor ama ilk satira bakarsak name keyinin bir Setting key oldugu ve String tipinde oldugunu gorebiliyoruz. Eger biz bu setting'i stringden Int'e cevirirsek sbt bunu sevmeyecektir. 

Ikinci key tipi olan task'lara ornek de package ve clean verilebilir. Package, projenin ana output artifact'ini temsil eder. Zaten inspect ile check edersek, bunun donus tipinin bir dosyas oldugunu goruruz. Diger bir task ise clean. Bunun da inspect edip bakarsak, tipinin Unit oldugunu goruruz yani birsey dondurmez ama description kismindan da anlasilacagi uzere build sonucu olusan dosyalari temizler. Son olarak da InputTask'a ornek olarak run gosterilebilir.

Aslinda anliyoruz ki sbt'nin kendi komutlari bile aslinda birer Task Setting seklinde tanimlanmis. Hatta nerede tanimlandiklari bilgisine de inspect ile ulasabiliyoruz:

sbt:sbt201> inspect run
[info] Input task: Unit
[info] Description:
[info]  Runs a main class, passing along arguments provided on the command line.
[info] Provided by:
[info]  ProjectRef(uri("file:/sbt101/"), "sbt101") / Compile / run
[info] Defined at:
[info]  (sbt.Defaults.configTasks) Defaults.scala:677

Default  setting'ler build.sbt dosyasina implicit olarak import edilir. Boylece bu default settinglere ulasabiliriz. 

import sbt.Keys._

seklinde. sbt shell'de ise tasks ve settings komutlarini kullanarak sirasi ile var olan task'lari ve daha genel olarak da tum settingleri gorebiliriz. 

Kendimize custom key
Peki kendi keyimizi nasil olsutrailibirz? Bunun icin sbt bize uc metot sunmaktadir:

1-  settingKey[T] : bir setting key olsuturur

2- taskKey[T]: bir task key olusturur

3- inputKey[T]: input key olusturur

build.sbt icerisine yeni bir tanimlama yaparak, emotion isimli bir setting tipinde key olsutralim.

name := "sbt201"
lazy val emotion = settingKey[String]("bugun nasilsin?")
emotion := "coheyi"

Ikinci satirda key tanimlamasini yaptik. Bu verdigimiz metin ise, o key'in aciklamasi yani degeri degil. Deger atamasini bir alt satirda yaptik. simdi reload cekip, inspect ile bakalim naisil olmus?

sbt:sbt101> ;reload ;inspect emotion
[info] Loading project definition from /sbt101/project
[info] Loading settings for project sbt101 from build.sbt ...
[info] Set current project to sbt201 (in build file:/sbt101/)
[info] Setting: java.lang.String = coheyi
[info] Description:
[info]  bugun nasilsin? 

Gorulecegi gibi String tipinde ve mevcut deger olarak coheyi degeri atanmis bir key'imiz oldu. Aciklamasini ve hangi dosya tarafindan olusturuldugunu da gorebiliyoruz. Bir de simdi task tanimlayalim. build.sbt icerisinde:

lazy val rastgele = taskSetting[Int]("salla bana bir sayi")
rastgele := scala.util.Random.nextInt

Hemen reload cekip inspect ile kontrol edersek Int tipinde bir task yaratilmis (hasa) oldugunu goruyoruz. sbt shell'de task'imizi direk calistirabilir ve rastgele sayiyi olusturabiliriz.

sbt:sbt201> rastgele
[success] Total time: 0 s, completed Jan 8, 2021, 7:48:32 AM
sbt:sbt201> show rastgele
[info] -2120346894
[success] Total time: 0 s, completed Jan 8, 2021, 7:48:37 AM

Ilk seferde uretilen sayiyi goremedik. Cunku task'i direk calistirinca uretilen sayi rastgele key'ine ataniyor aslinda ama biz bunu disaridan goremiyoruz. Herhagibir task'in outputunu gorebilmek icin show ile calistirmak gerekiyor. Birkac kere ust uste calistirdimizda da gorebiliriz ki her seferinde farkli bir rastgele sayi uretilmektedir. Bu da task ile setting arasindaki farki tekrar gosteriyor. Setting, her proje load oldugunda tek bir sefer atanirken, task her cagrildiginda tekrar atanmaktadir. 

Iliskiler
Simdiye kadar sbt tarafindan saglanan setting ve task'lari gorduk hatta kendimiz de olusturduk. Ancak bunlarin arasinda bir iliski ortaya koymadik. Ama gercek projelerde bunlar arasinda iliskiler mevcuttur. En basitinden bir dependency iliskisi olabilir. Yani bir setting baska setting'lere bagli olarak tanimlaniyor olabilir.  Benzer sekilde tasklar da birbirlerine veya baska bir setting'e depend ediyor olabilir. Yalnizca, bir setting baska bir task'a depend edemez. Cunku setting'ler proje load'da sadece tek sefer calisirken tasklar her cagrildiklarinda tekrar calistirilirlar. Bu durumda bir setting, bir taska depend ediyor olsa idi, task'in eski bir degeri ile kalmasi ve tum sistemin inconsistent olmasina sebep olabilirdi. 

buils.sbt dosyasina bir bagli (dependent) setting ekleyelim:

lazy val emotion = settingKey[String]("bugun nasilsin?")
emotion := "coheyi"

val status = settingKey[String]("durumumuz nasil?")
status := {
    val e = emotion.value
    s"Super ve $e"
}

daha sonra sbt shell'de reload ve ardindan inspect status ile inceleme yapalim. 

sbt:sbt201> status
[info] Super ve coheyi

Goruyoruz ki dependent settingimiz gayet guzel calisiyor. 

Neler ogrendik?

- sbt shell ile calismak

- sbt klasor yapisi 

- build definition settings (yani build.sbt)

- kendi custom build settingslerimizi olusturduk

- build definition icerisinde task graph incelemesi (bagli keyler)

bir sonraki postta sifirdan bir proje uzerinde lifecycle uzerinde duracagiz. 

Simdilik hoscakalin.

Yorumlar

Bu blogdaki popüler yayınlar

Python'da Multithreading ve Multiprocessing

Threat Modeling 1

Encoding / Decoding