Give Stack a Shot
Stack 簡單筆記
背景知識
projects
Stack 是以 project centralized 的方式在管理套件。所以 Stack 不認為在某 haskell project 之中時,它會切換到 implicit global project 模式,而此時東西會安裝到 ~/.stack/global-project/
裡面去。反之,如果 Stack 可以找到 <project name>.cabal
或是 stack.yaml
,它就會原地建立一個該 project 專用的編譯環境。這通常就是產生一個專用資料夾 .stack-work/
然後把所有相關的設定和套件放在裡面。
packages
在 .cabal 檔中,我們會定義出 project 所需要的 build-depends,而 Stack 就會根據這項資訊來計算(resolve)所需要的 packages 有哪些。但是在進一步了解 Stack 怎麼計算 dependencies 之前,我們要先知道 Stack 是怎麼“認識” packages 的。Stack 將一個 project 會需要的 packages 分成三種:
- local packages 係指我們自己寫在 local 的 projects,這是由我們自己定義與維護的東西。使用時需要在
stack.yaml
中的packages
欄位中告訴 stack 這些 packages 的 remote 或是 local path。 - curated packages Stack 每次在編譯時會先嘗試使用預先準備好的常用套件組合。這些套件組合被稱 snapshots,而一個 snapshot 基本上就是整個 hackage 的某個 subset,而在該 subset 中所有套件都是以「不會彼此衝突」為前提所選出。這些被 snapshot 所定義的套件就被稱為是 curated packages,更多細節請參考下面有關 snapshot 的部份。
- extra-dep packages 凡是不存在於當前 snapshot 但是 Stack 可以在 hackage (或其他 indices) 中找到的套件。
在預設的情況下,Stack 只會幫我們找出最適合的 snapshot 並且只安裝 curated packages。local packages 和 extra-dep packages 都要另外描述在 stack.yaml
裡面。
對 cabal 來說上面三種都是 dependencies,但是對 Stack 來說只有後面兩種比較容易會被稱為 dependencies。不過這點也不是絕對的,文件中還是可以看到一些相似但是好像不太一致的術語,所以這點僅供參考。
Snapshots
Stack 的核心關鍵(之一)是 snapshot。一個 snapshot 是一組預選好(而且沒有版本打架)的 version-specific 套件組合。整個 stack 系統就是建立在一大群預設好的snapshot 之上。這些不同的 snapshot 就如同字面上的意思,提供了我們所有 packages 在某個時間點的某種快照。而我們未來可以使用和之前同樣的一組 packages 去編譯我們的 project。Stack 有兩種 snapshots 可以用:LTS (long term support) Haskell 和 Stackage Nightly。前者一段時間推出一次是穩定版本,後者是每天更新的稍微沒那麼穩定版。
對某個 snapshot 來說,並不是所有其中敘述到的 packages 就都會被預先安裝到系統中。只有曾經被使用過的才會被安裝,這點就跟 cabal 還有其他任何套件管理程式差不多。目前已經被安裝過的 curated packages 會儲存在 ~/.stack/snapshots/
下面。如果要查詢 packages 清單,
會根據目前 Stack 所能偵測到的 project 來列出所有當前可以存取的 packages。
Build-Plan and Resolvers
Stack 會為每一次 build 選擇一套 build plan,其中包括這次 build 所使用的編譯環境(包括 ghc 版本、ghc 參數、path等等)和一組 snapshot (裡面會定義可以被使用的 packages 有哪些)。這些 build plan 在 stack 裡面被稱為 resolver (因為我們會根據這裡面的資訊來做 dependency resolve)。所有 resolver 都會儲存在 ~/.stack/build-plan/
裡面。
初探
Stack 和 git 一樣是非常 project-based 的東西,它的操作流程如下並不複雜,不過根據目前 project 的狀態可以分成下列幾種情境:
[情境A] 什麼 project? 我根本什麼都沒有呀!
我們可以用老方法:寫點 code 然後 cabal init
,這樣就有一個 hackage project (然後就可以跳到 [情境C] 那節)。 或者,我們可以直接用 stack 來產生一個新的 stackage project!
Stack 有內建一些 project template 來讓我們可以快速產生一個完整的 project dir,
有被用過的 templates 都會下載並放在 ~/.stack/templates/
。可以用
來查詢所有可以用的 templates。另外,下列是個人覺得好用/常用的基本款 templates :
- new-template
- simple
- simple-library
- haskeleton
- hakyll-template
- servant
因為 stack new
會幫我們產生 stack.yaml
檔案,所以一些相關的參數也都可以用,例如說, 我們也可以在 new 的時候同時指定我們想要用的 resolver 版本;也可以強制跳過有問題 packages 等等
[情境B] 我有一個 project (沒 .cabal 檔)
寫你個好 .cabal 然後直接進入下一節。
[情境C] 我有一個 hackage project (有 .cabal 檔)
如果已經有寫好 .cabal 檔,我們可以用
來根據那個 cabal 檔案來產生 stack.yaml
。 Stack 預設會嘗試找出最佳 snapshot 來滿足 .cabal 上面描述的 build-depends。這其實有點煩人,因為這個找出最佳 snapshot 的動作其實蠻花時間的,所以如果我們就只是想用某個夠新或是慣用的 resolver ,我們也可以直接強制指定:
雖然說 snapshot 應該包不少 packages 了,但是其實還是有機會我們會要用某些 hackage 上面有但是 snapshot 沒有的東西。 這種時候 stack init
會哭哭類似
Resolver ‘lts-6.2’ does not have all the packages to match your requirements.
這樣的話。這種時候我們可以要求 stack init
去 hackage 上面幫我們找看看有沒有那些缺少 packages 可以用:
如果那些需要但是不在 snapshot 裡面的 packages 有在 hackage 裡面 (也就是說,他們是 extra-dep packages),那 stack init
會自動把這些 packages 放在 stack.yaml
的 extra-deps
這個欄位之中。如果還是有 packages 找不到,那有兩種可能,一種是「你的 index 太舊了」這種時候我們就可以先試試看 stack update
看看能不能解決問題。如果還是不行,那八成就是因為你用了 local packages,這種時候我們就要先略過那些有問題的 packages 把 stack.yaml
生出來:
然後再在 stack.yaml
裡面的 packages
欄位中告訴 Stack 要去哪裡找這些東西 (請參考這段文章)。最後再叫 Stack 根據更改過的 stack.yaml
重新安裝並檢查編譯環境是不是都 ok 了:
有時候我們會需要在已經有 stack.yaml
時需要重新 init 一份,這種時候我們可以在 stack init
後面加上參數 --force
。這樣就會讓目前 resolve 的結果強制蓋過之前的。
[情境D] 我有一個 stackage project (有 .cabal 和 stack.yaml 檔)
不管是原本就有 stackage project 或是根據上面三種情境來生成 stack.yaml
檔,一旦準備好了以後我們就可以開心地根據那個 local yaml config 檔來 build 我們的 stackage:
其中,因為 Stack 會順便幫你管一下包含 ghc 版本在內的編譯環境,而 stack setup
這的指令就是告訴 Stack 根據 local stack.yaml
去「幫我看一下這個 project 要用的 build-plan (i.e. resolver 的內容)是不是準備妥當了」。基本上就類似 cabal configure
這樣的指令。
關於 GHCs
因為不同 build plan 會用到不同版本的 ghc,所以 Stack 會順便幫我們安裝並且管理各種不同的 ghc。 不過要注意的是,Stack 並不會把 system path 設定到這些 ghc 身上, 所以除非我們另外用其他當是安裝 ghc 否則 console 裡面是沒有 ghc 可以用的。 但是我們可以透過
這些指令來使用。
問題與待續
- 自己定義 snapshot
- local project 怎麼使用 global-project 裡面的 package?
packages
+ extra-dep: true 和extra-deps
到底差在哪?