Stack (작성 중)

Posted on March 30, 2025

The Haskell Tool Stack

stack.yaml

###### Project-specific

snapshot: lts-23.14
# snapshot: ghc-9.8.4 스냅샷 없이 컴파일러와 같이 배포된 패키지 이용한다.
# resolver는 snapshot의 동의어, 둘 중 하나만 지정한다.

packages: 
- my-package
- dir3/my-other-package
# 현재 프로젝트가 가진 패키지 목록
# packages: . 기본값으로 싱글 프로젝트 패키지를 의미

extra-deps:
# 스냅샷에 없는 패키지나 버전. 같은 이름의 스냅샷 패키지는 가려진다.
# @TODO 팬트리: (영단어 뜻은 찬장 같은 것) 스냅샷에 없는 것들을 Hackage, Git, 로컬등에서 
#               가져와 써먹고 캐싱해 둔 걸 팬트리 패키지라 부르는 것 같다.
#               ※ 특이사항: GHC boot 패키지는 여기다 써봐야 무시된다고 한다.

ghc-options:

flags:
  package-name:
    flag-name: true
# 명령 줄 옵션과 동일
# 스냅샷 패키지에 지정된 모든 Cabal 플래그 설정을 덮어씌운다.

drop-packages:

user-message:
# stack이 이 설정 파일을 읽어들일 때마다 출력

custom-preprocessor-extensions:

extra-package-dbs:

curator:
# 2025.3 아직은 실험적인 필드

###### Non-project specific

allow-newer: false
# cabal 파일에 있는 버전 제약 무시

allow-newer-deps: false
# 위 allow-newer 설정이 적용될 하위 패키지들 지정

arch:
# stack이 동작하는 머신 아키텍처

compiler:
# 스냅샷에 있는 컴파일러 버전을 덮어 씌운다.

extra-lib-dirs:
# 빌드 환경 구성을 위해 필요한 라이브러리를 찾을 경로 

hpack-force:
# .cabal 파일이 수작업으로 수정됐을 때, hpack이 덮어 쓸지 여부
# hpack 0.20부터는 덮어 쓰기 거부가 기본값

install-ghc:
# GHC를 다운로드하고 설치할지 여부.

local-bin-path: ~/.local/bin
# stack build --copy-bins(stack install과 동일)이 패키지를 설치할 경로

nix:
  enable: false
  pure: true
  package: []
  shell-file:
  nix-shell-options: []
  path: []
  add-gc-roots: false

system-ghc: true
# 시스템에 있는 (Path에 잡혀 있는) GHC를 쓸지 말지 여부

with-hpack: /usr/local/bin/hpack
# stack 빌트인 hpack이 아닌, 별도 hpack 실행 파일을 쓴다.

work-dir: .stack-work

stack new sample
sample/

.
├── app
│   └── Main.hs
├── CHANGELOG.md
├── LICENSE
├── package.yaml
├── README.md
├── sample.cabal
├── Setup.hs
├── src
│   └── Lib.hs
├── stack.yaml
└── test
    └── Spec.hs

Stack work 디렉토리

Stack work directory

특별히 옵션을 주지 않으며, 빌드 중에 필요한 파일들과, 최종 빌드된 바이너리 모두 이 아래 위치합니다. (한 눈에 살펴 본적이 없었는데, 많이도 생성됩니다. stack build, stack install 후의 모습입니다.)
.stack-work/

.
├── dist
│   └── x86_64-linux-nix
│       └── ghc-9.6.4 ----* stack path --dist-dir로 확인
│           ├── build
│           │   ├── autogen
│           │   │   ├── cabal_macros.h
│           │   │   ├── PackageInfo_sample.hs
│           │   │   └── Paths_sample.hs -----------------* Paths_pkgname 모듈
│           │   ├── Lib.dyn_hi
│           │   ├── Lib.dyn_o
│           │   ├── Lib.hi
│           │   ├── libHSsample-0.1.0.0-2tRkpRkMrLYKl9DJwHDpTe.a
│           │   ├── libHSsample-0.1.0.0-2tRkpRkMrLYKl9DJwHDpTe-ghc9.6.4.so
│           │   ├── Lib.o
│           │   ├── Paths_sample.dyn_hi
│           │   ├── Paths_sample.dyn_o
│           │   ├── Paths_sample.hi
│           │   ├── Paths_sample.o
│           │   └── sample-exe
│           │       ├── autogen
│           │       │   ├── cabal_macros.h
│           │       │   ├── PackageInfo_sample.hs
│           │       │   └── Paths_sample.hs -----------------* Paths_pkgname 모듈
│           │       ├── sample-exe --------------* 실행 파일
│           │       └── sample-exe-tmp
│           │           ├── Main.hi
│           │           ├── Main.o
│           │           ├── Paths_sample.hi
│           │           └── Paths_sample.o
│           ├── build-lock
│           ├── package.conf.inplace
│           │   ├── package.cache
│           │   ├── package.cache.lock
│           │   └── sample-0.1.0.0-2tRkpRkMrLYKl9DJwHDpTe.conf
│           ├── setup-config
│           ├── stack-build-caches
│           │   └── d6bc1450919ab91c0c8fcf9d9e33fcc28df25df98110f462287e036fb400 2bc3
│           │       ├── exe-sample-exe
│           │       └── lib
│           ├── stack-cabal-mod ----* 수정 시각 확인 용도
│           ├── stack-project-root ----* 프로젝트 개발의 루트 디렉토리를 가진 txt 파일
│           └── stack-setup-config-mod
├── install
│   └── x86_64-linux-nix
│       ├── af9ab2fb7015c65f339c4bb931b57f680cb5ea6b6d26131227e597eb096d0623
│       │   └── 9.6.4
│       │       └── pkgdb
│       │           ├── package.cache
│       │           └── package.cache.lock
│       └── d6bc1450919ab91c0c8fcf9d9e33fcc28df25df98110f462287e036fb4002bc3
│           └── 9.6.4 ----* stack path --local-install-root로 확인
│               ├── bin
│               │   └── sample-exe  --------------* 실행 파일
│               ├── doc ----* stack path --local-doc-root로 확인
│               │   └── sample-0.1.0.0
│               │       └── LICENSE
│               ├── lib
│               │   └── x86_64-linux-ghc-9.6.4
│               │       ├── libHSsample-0.1.0.0-2tRkpRkMrLYKl9DJwHDpTe-ghc9.6.4. so
│               │       └── sample-0.1.0.0-2tRkpRkMrLYKl9DJwHDpTe
│               │           ├── Lib.dyn_hi
│               │           ├── Lib.hi
│               │           ├── libHSsample-0.1.0.0-2tRkpRkMrLYKl9DJwHDpTe.a
│               │           ├── Paths_sample.dyn_hi
│               │           └── Paths_sample.hi
│               ├── pkgdb ----* stack path --local-pkg-db로 확인 
│               │   ├── package.cache
│               │   ├── package.cache.lock
│               │   └── sample-0.1.0.0-2tRkpRkMrLYKl9DJwHDpTe.conf
│               └── share
│                   └── x86_64-linux-ghc-9.6.4
│                       └── sample-0.1.0.0
│                           └── config.txt  --------------* 데이터 파일
├── stack.sqlite3
└── stack.sqlite3.pantry-write-lock

Hpack

Hpack: Cabal 패키지 형식 .cabal을 생성하기 위한 전처리기 같은 역할입니다. Hpack 형식은 패키지들을 package.yaml 파일에 기술합니다. cabal2nix와 stack은 네이티브로 hpack을 가지고 있어, 이 파일을 해석할 수 있습니다. 단독 CLI 도구 hpack으로도 이 파일을 읽어 .cabal 파일을 생성할 수 있습니다. Hpack 형식과 Cabal 형식 둘을 비교하면, 크게 달라 보이진 않는데, 공통된 부분을 한 번만 작성해서 libararay:, tests:, executables: 등의 타겟으로 모두 쓸 수 있다든지, 약간 더 편의성이 높은 정도로 보이는데요. Hpack 형식을 변환 없이 그대로 주 형식으로 선택했어도 될 것 같은데, 호환성이 때문에 Cabal 형식을 유지하고 있는 게 아닐까 추측합니다. 어쨌든, Cabal 형식을 작성하는 것보다는 아주 조금 더 편리합니다.

Paths_ 모듈의 동작

Cabal은 모든 모듈마다 Paths_ 모듈을 각 각 생성합니다. 이 들 모듈들에 대해서 어떤 동작을 할지는 spec-version 필드 값에 따라 달라집니다.

모던 동작을 하려면 적어도 0.36.0 이상이어야 하고, 이보다 낮으면 레거시 동작을 합니다.

spec-version: 0.36.0

모던 동작

※ Executable, Libary, Test 같은 것들을 컴포넌트라 부릅니다.
컴포넌트를 위해 Paths_ 모듈을 사용하길 원하면, 명시적으로 generated-other-modules에 지정해야 합니다.

library:
  source-dirs: src
  generated-other-modules: Paths_name # name 부분을 패키지 이름으로 바꿉니다.

레거시 동작

역사적인 이유로, Hpack은 .cabal 파일을 생성할 때 Paths_ 모듈을 other-modules에 추가합니다.
이러지 않게 하려면, package.yaml에 다음과 같이 설정합니다.

library:
  when:
  - condition: false
    other-modules: Paths_name

Paths_ 모듈

7.6 Accessing data files from package code
data-files 필드에 써 준 파일들의 위치는 시스템마다 다를 수 있습니다. 어떤 경우는 설치 후 파일을 이동할 수도 있습니다. 패키지가 이런 데이터 파일을 런타임에 찾을 수 있게 하기 위해 Cabal이 Paths_pkgname이란 모듈을 생성합니다. 예를 들어 패키지가 data/words.txt란 데이터 파일을 참조하는데, 이 파일을 패키지에 포함시키더라도, 패키지가 설치될 곳을 미리 알 수 없기 때문에 하드 코딩할 수 없습니다. 이럴 때 Paths_pkgname 모듈을 이용해 설치 경로를 동적으로 얻을 수 있습니다.
※ pkgname에 있는 하이픈은 모두 언더스코어_로 바뀝니다.
Paths_pkgname은 아래 함수를 가지고 있습니다.

getDataFileName :: FilePath -> IO FilePath

Paths_pkgname 모듈을 사용하려면, other-modules와 autogen-modules에 적어 줘야 합니다. Paths_pkgname 모듈은 다른 자동 생성 모듈처럼 플래폼 독립적이지 않기 때문에, sdist가 생성한 소스 tarballs에 포함되지 않습니다.

※ stack sdist Hackage에 등록 가능한 형태로, 패키지 아카이브 파일을 만듭니다.

예시

config.txt

Data contents

package.yaml의 탑레벨에 아래를 추가합니다.

data-files: config.txt

src/Lib.hs

module Lib
    ( someFunc
    ) where

someFunc :: IO ()
someFunc = putStrLn "someFunc"

app/Main.hs

module Main (main) where

import Paths_sample (getDataFileName, getDataDir)
import Lib
main :: IO ()
main = do
  someFunc
  dir <- getDataDir
  filePath <- getDataFileName "config.txt"
  content <- readFile filePath
  putStrLn $ "Data dir: " ++ dir
  putStrLn $ content ++ " in \n" ++ filePath

그리고 stack sdist하면, .cabal에 아래가 추가된 걸 확인할 수 있습니다.

data-files: 
  config.txt
...
library
  ...
  other-modules:
      Paths_sample
  autogen-modules:
      Paths_sample
   ...
executable
  ...
  other-modules:
      Paths_sample
  autogen-modules:
      Paths_sample

Cabal이 빌드 과정에서 자동으로 생성해서 바이너리에 포함시킵니다. 빌드에 임시로 생성한 폴더인 /home/lionhairdino/workroom/stack_sample/sample/.stack-work/dist/x86_64-linux-nix/ghc-9.6.4/build/sample-exe/autogen 폴더에서 Paths_sample.hs 파일을 볼 수 있습니다.

실제 데이터 파일의 위치는, 저의 경우는 다음과 같이 잡혔습니다.(stack은 nix가 활성화된 상태입니다.)

someFunc
Data dir: /home/lionhairdino/workroom/stack_sample/sample/.stack-work/install/x86_64-linux-nix/d6bc1450919ab91c0c8fcf9d9e33fcc28df25df98110f462287e036fb4002bc3/9.6.4/share/x86_64-linux-ghc-9.6.4/sample-0.1.0.0
Data contents
 in 
/home/lionhairdino/workroom/stack_sample/sample/.stack-work/install/x86_64-linux-nix/d6bc1450919ab91c0c8fcf9d9e33fcc28df25df98110f462287e036fb4002bc3/9.6.4/share/x86_64-linux-ghc-9.6.4/sample-0.1.0.0/config.txt

실행 파일의 위치는 다음과 같습니다.

/home/lionhairdino/workroom/stack_sample/sample/.stack-work/dist/x86_64-linux-nix/ghc-9.6.4/build/sample-exe

실행 파일 위치와 데이터 파일의 위치를 나란히 보면 다음과 같습니다.

dist/x86_64-linux-nix/ghc-9.6.4/build/sample-exe/sample-exe
install/x86_64-linux-nix/해시/9.6.4/share/x86_64-linux-ghc-9.6.4/sample-0.1.0.0/config.txt

자동 생성된 Paths_sample.hs 파일을 열어 보면,

getBinDir     = catchIO (getEnv "sample_bindir")     (\_ -> return bindir)
getLibDir     = catchIO (getEnv "sample_libdir")     (\_ -> return libdir)
getDynLibDir  = catchIO (getEnv "sample_dynlibdir")  (\_ -> return dynlibdir)
getDataDir    = catchIO (getEnv "sample_datadir")    (\_ -> return datadir)
getLibexecDir = catchIO (getEnv "sample_libexecdir") (\_ -> return libexecdir)
getSysconfDir = catchIO (getEnv "sample_sysconfdir") (\_ -> return sysconfdir)

런타임에 환경 변수를 읽어, 각 디렉토리들을 바꿀 수 있음을 알 수 있습니다.

stack_sample/sample/result> sample_datadir=. ./sample-exe 
someFunc
Data dir: .
Data contents
 in 
config.txt

@TODO
Paths_sample.hs를 열어 보면, 위 디렉토리값들이 현재 개발 중인 상황에서 build했을 때 쓰인 폴더명들이 모두 하드 코딩되어 있다. 이러면, 최종 바이너리에 개발 환경 흔적(환경 변수가 설정되어 있지 않을 때 쓸 기본값)이 남는 것 아닐까?

마무리

몇 년 전에는, 생태계에서 꽤 중요한 요소인듯한데, (아마도 규모가 그리 크지 않은) 하나의 상업 회사가 이끄는 게 불안하지 않나 생각했었는데요. 지금은 Commercial Haskell 그룹에서 후원을 받아 개발되고 있다고 합니다.

Github 계정이 없는 분은 메일로 보내주세요. lionhairdino at gmail.com