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 패키지들은 package.yaml 파일에서 기술합니다. cabal2nixstack은 네이티브로 hpack을 가지고 있어, 이 파일을 해석할 수 있습니다. 단독 CLI 도구 hpack으로도 이 파일을 읽어 .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-modulesautogen-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