아래 페이지들을 보며 스케치 중인데, 아직은 틀리게 해석, 이해한 곳도 많이 있는 것 같습니다. 교차하며 보며 조금씩 수정하고 있습니다. 2025.3
하스켈 언어 프레임 워크
mwhwombat-haskell-dev
haskell4nix
Serokell 블로그 - 실용 닉스 플레이크
haskell.nix
srid/haskell-flake
nixos.asia
nixos.org 문서의 하스켈 섹션
NixOS Wiki
닉스 하스켈 생태계는 크게 Nixpkgs의 haskellPackages(이전 haskell4nix) 접근과 IOHK의 haskell.nix로 나누어진다. haskell.nix는 Nixpkgs 바깥에 존재한다.
로컬에서는 /nix/store/해시-source/pkgs/development/haskell-modules
에서 닉스 소스 코드를 확인할 수 있다.
derviation을 생성하고, 개발쉘을 준비하는 기본적인 닉스 코드 모양은 아래와 같다.
let
pkgs = import <nixpkgs> { };
in
{
package = pkgs.stdenv.mkDerivation { ... };
shell = pkgs.mkShell { ... };
}
mkDerivation
, mkShell
을 하스켈 패키지와 쉽게 쓸 수 있도록, 이들을 래핑한 함수들과 특별한 구조들을 만들어 둔, 하스켈 프레임워크가 준비되어 있다. 하나의 파일이, 하나의 derivation을 생성하는 것이 아니라, derivation은 패키지 단위로 준비한다. 만일, 하스켈 프로젝트가 비하스켈 의존성이 있다면, mkDerivation
과 mkShell
로 준비하고, 이 것들을 하스켈 프레임워크의 래핑 함수들이 가져다 쓴다.
빠르게 보기
> nix-env -i cabal2nix cabal-install
> cabal2nix . > default.nix
> cabal2nix --shell . > shell.nix > nix-shell --command 'cabal configure'
cabal2nix
프로젝트 .cabal
파일에서 의존성 정보를 가져오고, nixpkgs
가 가진 haskellPackages
컬렉션에서 원하는 것들을 가져오며 의존성을 풀어갑니다.
하스켈만을 위한 툴 ghc
,cabal-install
, stack
, hpack
, niv
, cachix
, …
위 툴들은 특별히 설치하지 않아도 nixpkgs
탑레벨에서 제공 된다.
callCabal2nix name src args
cabal2nix
를 써서 src
소스 디렉토리 안에 있는 .cabal
파일을 닉스 표현식으로 바꾸고, 이 표현식을 haskellPackages.callPackage
에 넘겨 name
이름의 하스켈 패키지 derivation
을 생성한다. haskellPackages.callPackage
에 추가적으로 넘길 인자가 있으면 args
를 쓴다.
callCabal2nixWithOptions name src opts args
opts
가 문자열이면 cabal2nix
의 특별 명령줄 인자로 쓰인다.
extraCabal2nixOptions
srcModfier
overrideCabal f drv
기존 하스켈 패키지의 cabal 정의에 새로운 속성을 추가할 때 쓴다. 결과적으로 수정된 새로운 패키지를 생성한다.
callHackageDirect
닉스에서 하스켈 패키지를 직접 가져오는 함수. 특별히 뭔가를 하지 않고, 그냥 Hackage에서 다운로드 해서, 닉스 빌드 시스템에 통합시킨다. Hackage에 있는 정확한 버전을 고정. 하스켈에서 하지 않고, 닉스단에서 지정하는 방식이라 nix-stroe에 저장되니, 재다운로드를 덜 하게 된다.
doJailbreak
doJailbreak = overrideCabal (drv: { jailbreak = true; });
/nix/store/해시-source/pkgs/development/haskell-modules/lib/compose.nix
파일에서 소스 확인
jailbreaking : 패키지들의 모든 버전 제약을 날린다. 각 패키지 집합은, 각 의존성의 단 하나의 버전만 포함하니, 어차피 안전한 작업이다. 안전한 이유는, 닉스의 패키지 집합에는 여러 버전이 공존하지 않고, 오직 하나의 버전만 들어 있다. 예를 들어, pkgs.haskllPackage
에 text
패키지가 있다면, 그 안에는 어차피 text-2.0.2
같은 하나의 버전만 존재한다. 이렇기 때문에 버전을 제거해도 달리지는 게 없다는 뜻이다. 보통 cabal에서는 패키지마다 의존 버전 범위를 지정하지만, Nix 패키지에서는 의미 없다.
derivation의 속성 중 jailbreak
를 true
로 놓으면, 버전을 제거한다.
dontCheck
테스트를 통과하지 못해 빌드를 실패할 경우, 테스트를 건너 띄게 할 수 있다.
jailbreakUnbreak
다른 패키지들의 버전은 그대로 유지하고, cabal-install
패키지의 버전 제약만 제거한다.
NixOS Wiki
얼마나 복잡한 하스켈 프로젝트냐에 따라 선택할 수 있는 다양한 방법이 있다. 서브 프로젝트가 없으면 developPackage, 서브 프로젝트가 있고 여러 팀이 작업하면 haskell.nix, 서브 프로젝트가 있고 단일팀이면 shellFor.
#!/usr/bin/env nix-shell
#!nix-shell --pure -i runghc -p "haskellPackages.ghcWithPackages (pkgs: [ pkgs.turtle ])"
= do
main putStrLn "Hello World"
닉스를 쓰지 않을 때처럼 cabal
을 쓸 수 있다.
$ nix-shell -p "haskellPackages.ghcWithPackages (pkgs: with pkgs; [ cabal-install ])"
$ cabal init $ cabal run
다른 라이브러리 의존성이 있을 땐, nix-shell
의 옵션으로 추가할 수 있다.
닉스 안에서 stack을 이용해서 프로그램을 패키징하고 싶으면, stack build
를 부르는 stdenv.mkDerivation
의 래퍼인 haskell.lib.buildStackProject
을 쓴다. stack
이 필요한 것들을 다운로드 받아야 하기 때문에 sandbox를 비활성화한다. nix-build --option sandbox false
. 아래 예시는 R
, zeromq
, zlib
를 쓰는 프로젝트를 stack
으로 빌드한다.
with (import <nixpkgs> { });
{ # buildStackProject가 stack build를 부른다.
haskell.lib.buildStackProject name = "HaskellR";
buildInputs = [ R zeromq zlib ];
}
stack
대신 닉스를 사용하여 재현 가능한 방식으로 의존성을 관리할 수 있다. stack
이 의존성에 맞는 집합을 찾기 위해 solver를 사용하는 반면, 닉스는 고정된 패키지 집합을 쓴다. 닉스가 제공하는 캐싱 시스템의 장점을 누릴 수 있다. 닉스도 내부적으로 cabal을 쓴다.
$ nix-shell -p "haskellPackages.ghcWithPackages (pkgs: with pkgs; [ cabal-install ])" --run "cabal init"
※ cabal-install
은 디폴트로 가지고 있는 패키지라 위에처럼 지정하지 않아도 되는데, 특이한 환경에서는 cabal-install
이 없는 경우도 있을 수 있으니, 확인 차원에서 명시적으로 넣어주는 것도 나쁘지 않다고 한다.
※ 로컬에 있는 특정 라이브러리를 사용하려면, cabal.project
파일에 해당 라이브러리 경로를 지정하면 된다.
default.nix
파일에 다음을 추가한다.
let
pkgs = import <nixpkgs> { }; # 재현성을 높이려면 채널을 고정한다.
# flake에서 devShell을 빌드하려면 아래를 추가한다.
# 보통 lib.inNixShell을 써서 자동으로 감지되는데, flake에선 작동하지 않는다.
# returnShellEnv = true;
in
{
pkgs.haskellPackages.developPackage root = ./.; # 루트에 있는 프로젝트를 위한 개발 환경을 만든다.
}
developPackage
의 옵션 확인 pkgs/development/haskell-modules/make-package-set.nix. 이 함수는, 개발 쉘 설정할 때 필요한 함수들을 추가한 callCabal2nixWithOptions
의 래핑 함수다.
닉스는 의존성 이름을 받아 오고, 닉스가 가지고 있는 하스켈 패키지를 쓰기 위해, 자동으로 *.cabal
에 있는 build-depends
필드를 읽어 들인다. 닉스 저장소에 있는 하스켈 패키지 중 깨진 것들이 있다. 닉스는 라이브러리의 최신 버전만 가지고 있어, 구버전을 필요로 하는 패키지들은 깨진다. 이럴 때는 overrides
와 source-overrides
속성을 이용한다. source-overrides
속성은 로컬 라이브러리를 불러 오기에 적당하다.
let
pkgs = import <nixpkgs> { };
in
{
pkgs.haskellPackages.developPackage root = ./.;
source-overrides = {
mylibrary = ./mylibrary;
};
}
하지만, 위와 같이 하면, mylibrary
가 완전히 컴파일되기 전에 쉘에 들어갈 수 없으므로, 여러 프로젝트에서 동시에 작업하려면 shellFor
가 필요하다.
또한, 컴파일할 때 필요한 도구나 실행시 필요한 라이브러리를 추가해야 할 수도 있다. 이를 위해 modifier
필드를 사용할 수 있으며, 이는 최종 하스켈 패키지에 적용할 함수다. (특히, 위에서 본 overrideCabal
을 적용할 수도 있다.)
pkgs.haskell.lib.addBuildTools
를 사용하여 nativeBuildInputs
을 추가할 수 있고, pkgs.haskell.lib.addExtraLibraries
를 사용하여 buildInputs
를 추가할 수 있다.
let
pkgs = import <nixpkgs> { };
in
{
pkgs.haskellPackages.developPackage root = ./.;
# 최종 패키지 derivation에 변형을 가하는 함수를 지정한다.
# 아래는 drv: ~ 람다 함수 형태를 지정하고 있다.
# developPackage가 생성한 하스켈 패키지 derivation이 drv로 넘어간다.
modifier = drv:
(with pkgs.haskellPackages;
pkgs.haskell.lib.addBuildTools drv [ cabal-install
ghcid]);
}
shellFor
는 developPackage
와 비슷한데, 복수의 패키지를 동시(이 건 cabal.project
와 비슷)에 개발할 수 있게 조금 더 복잡한 기능을 가지고 있다. developPackage
와는 다르게 shellFor
는 derivation을 output
으로 가지고 있지 않다.
사용자 프로젝트를 “추가 하스켈 패키지”로 하스켈 패키지에 추가하기 위해, 하스켈 패키지 집합을 extend
하거나 override
한다. (예를 들어, haskellPackages.extend
와 packageSourceOverrides
를 사용하여, 프로젝트의 경로만 제공하면 이를 컴파일할 수 있다.) 그 후, haskellPackages.shellFor { packages = p: [p.myproject1 p.myproject2] }
를 사용하여 원하는 모든 패키지가 포함된 쉘을 생성하는 방식이다.
현재 프로젝트가 서브 폴더 ./frontend
와 ./backend
를 가지고 있을 때, 아래와 같이 cabal.project
를 생성한다.
packages:
frontend/
backend/
최종 shell.nix
파일을 만든다.
with import <nixpkgs> { };
(haskellPackages.extend (haskell.lib.compose.packageSourceOverrides {
frontend = ./frontend;
backend = ./backend;
}))
{
.shellFor packages = p: [ p.frontend p.backend ];
withHoogle = true;
buildInputs = [ pkgs.python pkgs.cabal-install ];
}
$ nix-shell $ cabal new-build all
예를 들어, nix-build
를 써서 backend
를 빌드하려면 default.nix
를 다음과 같이 만든다.
with import <nixpkgs> { };
(haskellPackages.extend (haskell.lib.compose.packageSourceOverrides {
frontend = ./frontend;
backend = ./backend;
})).backend
하나의 derivation을 생성하고 싶으면, 쉘을 시작하고 싶을 때는 쉘을 output
하고, 빌드하길 원할 때는 패키지를 output
하도록 if pkgs.lib.inNixShell then ... else ...
를 쓴다.
nixpkgs는 특정 프로젝트에 맞는 모든 버전 제약들을 해소하기 위해 솔버를 쓰지 않고, 하나의 패키지 집합만 유지하려고 한다.(stackage에 있는 패키지를 기반으로 하고, 그 외 패키지는 Hackage에서 최신 버전을 가져 온다.) 여러 이유로 깨진 패키지를 사용자가 고쳐 쓸 수도 있다. 첫 번째로, 현재 프로젝트가 쓰고 있는 GHC 버전으로 컴파일 했는지 확인한다. 현재 프로젝트의 버전을, haskellPackages
대신 haskell.packages.ghcXYZ
로 패키지를 컴파일할 때 쓰인 버전으로 바꿔 볼 수 있다.
developPackage
를 사용 중이면, 아래에서 설명하는 override
를 쓰든가, 간단하게 srouce-override
를 쓴다.
{
pkgs.haskellPackages.developPackage root = ./.;
source-overrides = {
# 현재 쓰고 있는 GHC haskellPackages가 1.6.0.0을 쓰고 있어 현재 제약과 맞지 않을 때
HUnit = "1.5.0.0";
};
};
source-overrides
에 아래 둘 중 하나를 넘긴다.
callHackage
에게 전달 된다. 닉스는 all-cabal-hashes
라는 패키지를 써서, 자동으로 해시를 찾으니, 해시를 지정할 필요가 없다. 만약 패키지가 all-cabal-hashes
에 없으면 callHackageDirect
를 쓴다.) ※ all-cabal-hashes
는 Hackage의 모든 패키지 정보와 각 버전들의 해시를 저장한 닉스 DB이다. callHackage
, callHackageDirect
는 Hackage에서 패키지를 가져오는 함수니, 해시가 필요 없을 것 같은데, 패키지를 가져온 후 현재 닉스 생태계에서 쓰이는 해시와 맞는지 확인하기 위해 필요하다.※ all-cabal-hashes
닉스는 반드시 해시로 패키지를 특정한다.
Hackage는 버전으로 관리된다.
Hackage에 패키지가 올라오면, 주기적으로 자동 감지하고 해시를 만들어 all-cabal-hashes
패키지에 담아두는 스크립트를 닉스팀이 만들어 뒀다.
닉스에서 Hackage에 있는 패키지가 필요할 때는, callHackage
함수에 버전만 넘기면, 알아서 all-cabal-hashes
에서 해시를 찾는다.
만일, 아직 all-cabal-hashes
에 해시가 없는 패키지에 접근할 일이 있을 땐, nix-prefetch-url
등으로 패키지를 다운받아 해시를 계산한 후 직접 callHackageDirect
의 sha256
필드로 넘겨줘야 한다.
경로 callCabal2nix
를 통해 패키지를 가져 온다. fetchurl
,fetchFromGitHub
같은 닉스 fetcher로 지정할 수 있다. 로컬 라이브러리라면, 해당 디렉토리를 지정하면 된다. ※ callCabal2nix
는 로컬 패키지나 Hackage가 아닌 곳의 패키지를 닉스 패키지로 변환할 때 쓴다. 변환할 때는 해시를 요구하지 않지만, 닉스 저장소에 저장할 때는 해시를 기반으로 저장된다.
derivation의 속성을 바꾸기 위해, 더 강력한 override 시스템을 쓸 수도 있다.
{
pkgs.haskellPackages.developPackage root = ./.;
overrides = self: super: {
random = pkgs.haskell.lib.overrideCabal super.random {
version = "1.1";
sha256 = "sha256-...";
doCheck = false;
}
}
}
developPackage
의 source-overrides
와 overrides
속성 차이는 다음과 같다.
source-overrides
패키지 소스 코드를 덮어 씌울 때 쓴다. 주로 버전 충돌을 해결하거나 로컬에서 수정된 패키지를 사용할 때.
-overrides = {
sourcemyPackage = /path/to/my/local/package
}
overrides
기존 하스켈 패키지를 수정할 때 사용. 패키지의 기본 설정을 덮어 씌우거나 , 패키지 내용을 변경하려는 경우.
※ 둘이 하는 일을 설명하면 다르다. overrides
는 패키지의 설정 중 소스 경로를 바꾸는 거고, source-overrides
는 패키지가 가진 소스 자체를 교체한다. 설명은 다른데, 최종 결과물은 같다.
ghcWithPackages
를 써서 override할 수도 있다. 아래는 라이브러리 quipper
가 올라오는 nix-shell
을 만드는 예시다.
{ pkgs ? import <nixpkgs> {} }:
with pkgs;
{
pkgs.mkShell buildInputs =
let
myHaskell = pkgs.haskell.packages.ghc884.override {
overrides = self: super: {
random = pkgs.haskell.lib.overrideCabal super.random {
version = "1.1";
sha256 = "sha256-...";
};
fixedprec = haskell.lib.markUpbroken super.fixedprec;
quipper = haskell.lib.markUpbroken super.quipper;
};
};
in
[
(myHaskell.ghcWithPackages (hpkgs: [
hpkgs.quipper]))
]:
}
mkShell
은 하스켈에 특화된 함수는 아니다. 닉스에서 개발 쉘을 생성하는 기본 함수다. overrideCabal
은 구 패키지의 속성을 수정한 새 패키지를 반환한다. 이 함수가 수정 가는한 옵션은 여기 소스를 참고한다.
mwhwombat 원문
많은 튜토리얼들이 niv, binary cache, flakes, Stack 등의 툴을 쓰라하는데, 빌드 툴을 최소화 해보자.
default.nix
를 아래와 같이 작성한다.
let
pkgs = import <nixpkgs> { };
in
{
pkgs.haskellPackages.developPackage # name = "패키지이름" # 현재 폴더명이 패키지 이름과 다를 경우, `name` 필드를 추가한다.
root = ./.;
source-overrides = {
grid = ../grid; # haskellPackages에 있는 grid 오버라이드
};
}
※ TODO ./
와 ./.
는 둘 다 현재 디렉토리를 뜻하고, 대부분의 경우 차이가 없다고 하긴 하는데? 아직 어떤 차이점이 있는지 모른다.
default.nix
파일에, 쉘에 들어갔을 때 쓸 패키지들의 버전을 지정할 수도 있고, cabal.project
에 지정할 수도 있다. 필요한 버전을 수정할 때 cabal.project
에 안 쓰고, default.nix
에 버전을 지정하면, 간단히 쉘을 빠져 나왔다 다시 들어가기만 하면, 새 버전으로 준비된 쉘로 들어 간다.
nix-build
를 실행nix-shell
을 실행. 모든 의존성이 준비된 상태의 쉘에 들어가고, cabal
명령어들을 쓸 수 있다.※ 별다른 옵션 없이 nix-shell
을 실행하면, default.nix
를 찾지만, 패키지를 직접 지정하는 옵션 -p
를 쓰면 default.nix
없이 환경에 들어갈 수있다. 처음 실행할 때는 지정한 패키지를 준비하느라 시간이 걸리지만, 한 번 준비한 이 후에는 nix-collect-garbage를 하기 전엔 바로 쉘로 들어간다.
하스켈 소스 파일에 아래 내용을 제일 처음에 추가한다.
※ #!
(Shebang) 현재 스크립트를 해석할 해석기를 지정한다. 그런데, 아래와 같이 하면 해석기안에서 또 해석기를 실행하는 것처럼 보인다. nix-shell
이 쉬뱅을 만나면 특별한 동작을 하도록 되어 있어 다음이 가능하다고 한다. 아래는 nix-shell
옵션을 여러 줄에 걸쳐서 표현 했을 뿐, nix-shell -p "haskell~~·" -i runghc
처럼 한 줄로 지정한 것과 같은 효과를 낸다.
#! /usr/bin/env nix-shell # 뱅! 닉스쉘로 실행하겠다는 뜻
#! nix-shell -p "haskellPackages.ghcWithPackages (p: [p.pandoc])"
#! nix-shell -i runghc
※ ghcWithPackages GHC와 지정 패키지를 함께 제공하는 유틸리티다.
(특정 ghc 버전에 추가 패키지를 넣어 올릴 수 있어서, 프로젝트별로 다른 GHC버전을 사용할 때 유용할 수 있다.
pure/impure nix-shell을 모두 지원, 여러 패키지들을 []
안에 지정하면, 지정 패키지들이 준비된 상태의 쉘에 들어간다)
※ -p
: 패키지를 지정하는 옵션인데, ghcWithPackages
가 pandoc
을 포함한 새로운 패키지를 생성하고, 그 패키지를 불러온다는 의미다
※ -i
: 명령어를 nix-shell 환경에서 실행한다.
※ 지금은 하스켈 관련 유틸리티를 쓰기 편하도록 gw
라는 이름의 래핑 유틸리티가 생겼다.
nix-shell
모드는 두 가지가 있다.
순수: nix-shell --pure
Nix에서 제공하는 패키지만 사용하고, 시스템 환경 변수나 글로벌 설정을 쓰지 않는다.
비순수 : nix-shell --impure
시스템 환경 변수도 접근 가능하다. 예를 들어 /usr/bin/ghc
가 있다면, 그 것도 사용 가능하다. 재현성은 더 떨어질 수도 있다는 걸 알고 쓰자.
※ Flake가 활성화된 닉스를 “flake-y” 닉스라 부르기도 한다.
최소한의 Nix언어 문법과, 간단한 Flake 설명 후에 하스켈 패키지 설치 방법에 대해 얘기한다.
세로켈 깃헙 저장소에서 제공하는, cabal2nix
를 쓰는 템플릿을 이용해, 기본적인 flake 파일을 생성한다.
nix flake init -t github:serokell/templates#haskell-cabal2nix
※ haskell-flake를 쓸 때는 srid 계정에 있는 템플릿을 쓰기도 한다.
nix flake init -t github:srid/haskell-flake
nix shell에서 실행이 필요한 패키지들을 $PATH
에 등록할 수 있다.
가능한 URI 모양
github:owner/repo/[revision or branch]
gitlab:owner/repo/[revision or branch]
https://example.com/path/to/tarball.tar.gz
git+https://example.com/path/to/repo.git
git+ssh://example.com/path/to/repo.git
file:///path/to/directory
/path/to/directory
./path/to/relative/directory flake-registry-value
어디에 설치 됐는지 궁금할 때
> command -v hello
hello 빌드에 필요한 모든 환경 설정이 같이 올라오게 하려면, nix develop
> nix develop nixpkgs#hello
> unpackPhase -- 소스 풀고
> cd hello-2.12.1
> configurePhase -- configure 스크립트를 돌리고 > buildPhase -- 빌드
nix profile (프로젝트별 환경 지정이 아니라, 계정에 종속된 환경을 만드는 nix-env
의 대체)
> nix profile install nixpkgs#hello
> nix profile list
> nix profile update hello > nix profile remove hello
nix flake
flake URI
를 받아 flake
의 출력을 트리 구조로 그려 준다.
> nix flake show github:nixos/nixpkgs
nix flake clone
flake 소스를 로컬 디렉토리에 복사한다. git clone
과 비슷.
nix flake lock
(이전 nix flake update
)
haskell.nix (첫 번째 커밋 날짜 2018-04-06 11:21:33 +0800) Cabal 프로젝트나 Stack 프로젝트를 Nix 코드로 자동 번역한다. Nixpkgs
의 haskellPackages
를 쓰지 않는, 독립적인 패키지 관리시스템으로 cabal-install
이나 stack
과의 호환성에 중점을 두고 설계되었다.
※ 이 동네 특징인 것 같은데, 설정 파일 이름도 haskell.nix
고, 이 haskell.nix
파일을 읽어 해석하는 Nix 모듈 이름도, 즉 시스템 이름도 haskell.nix
라 부른다. 컨텍스트에 따라서 뭘 가리키는지 알아 먹어야 하는 상황을 자주 만난다.
※ cabal-install: cabal
명령줄 프로그램을 가지고 있는 패키지. cabal이라 하면, cabal 스펙, cabal 라이브러리, cabal 도구(명령줄 프로그램 cabal
)을 모두 의미할 수 있지만, 보통 cabal이라 하면, 도구 cabal
을 의미하는 경우가 많다.
순환 의존성Cyclic dependencies
nixpkgs
에 있는 하스켈 빌더는 프로젝트의 라이브러리, 실행 파일, 테스트등을 개별 Nix derivation으로 분리하여 빌드한다. 이 때 라이브러리와 테스트가 서로 의존하는 경우 순환 의존성이 생겨 빌드에 실패한다. 라이브러리와 테스트가 공유하는 기능을 잘 빼내는 리팩토링을 하든가,doCheck
속성을false
로 해서 테스트 빌드를 비활성화 하거나,libraryHaskellDepends
속성을 사용하여 라이브러리 의존성을 명시적으로 지정할 수 있다.
nix-tools는 다음 하스켈 패키지들을 가지고 있다.
cabal-to-nix: 조건부 표현식을 유지retain?하며 .cabal
을 .nix
로 변환하는 트랜스포머.
※ 조건부 표현식 유지? - nix로 옮길 때 cabal에서 쓰던 조건부 표현식들이 사리지지 않는다는 뜻
stack-to-nix: stack.yaml
을 .nix
로 변환하는 트랜스포머. stack.yaml
에 있는 표현식을 읽어 haskell.nix
에 적합한 pkgs.nix
파일을 만든다.
plan-to-nix: plan.json
을 .nix
로 변환하는 트랜스포머. plan.json
을 읽어 pkgs.nix
파일을 만든다.
haskell.nix
하스켈 인프라를 위한 런타임 시스템이다.hackage.nix
Hackage에 있는 모든 cabal 표현식을 닉스 표현식으로 제공한다. hackage에 있는 패키지셋과 동기화하기 위해 주기적으로 업데이트 된다.stackage.nix
모든 stackage 스냅샷을 닉스 표현식으로 제공한다. hackage.nix
에 의존Niv
닉스 프로젝트 의존성을 트래킹하는 명령줄 도구다. flake를 쓰려면 안봐도 된다고 한다. 일단 패스.
haskell.nix는 stack.yaml
과 cabal.project
기반 프로젝트 모두에 작동한다.
let
# Niv 소스를 읽어들인다.
sources = import ./nix/sources.nix {};
# ./nix/sources.nix 파일이 없으면, 다음을 실행
# niv init
# niv add input-output-hk/haskell.nix -n haskellNix
# Niv로 고정한 haskell.nix commit을 가져 온다.
haskellNix = import sources.haskellNix {};
# haskellNix가 없으면 다음을 실행
# niv add input-output-hk/haskell.nix -n haskellNix
# nixpkgs를 불러오고 haskell.nix 에서 제공하는 nixpkgsArgs 전달한다.
pkgs = import
# haskell.nix provides access to the nixpkgs pins which are used by our CI,
# hence you will be more likely to get cache hits when using these.
# But you can also just use your own, e.g. '<nixpkgs>'.
-unstable
haskellNix.sources.nixpkgs# These arguments passed to nixpkgs, include some patches and also
# the haskell.nix functionality itself as an overlay.
;
haskellNix.nixpkgsArgsin pkgs.haskell-nix.project {
# 'cleanGit' cleans a source directory based on the files known by git
src = pkgs.haskell-nix.haskellLib.cleanGit {
name = "haskell-nix-project";
src = ./.;
};
# 사용할 GHC 버전 지정.
compiler-nix-name = "ghc925"; # `stack.yaml`기반 프로젝트에선 필요 없다.
}
아래는 Reflex 공식 홈에 있는 예시를 빌드하는 haskell.nix
이다.
※ 2025.3 master 브랜치가 아니라, enhance/ob-9.0.1 브랜치를 가져와서 빌드 성공했다.
{ obelisk ? import ./.obelisk/impl {
system = builtins.currentSystem;
iosSdkVersion = "10.2";
}
}:
with obelisk;
./. ({ pkgs, hackGet, ... }:
project with pkgs.haskell.lib; { # 하스켈 프로젝트를 빌드할 때 쓰는 라이브러리
# 아래, packages, overrides 같은 속성들은 Nixpkgs에 정의된 하스켈 관련 스펙이다.
android.applicationId = "systems.obsidian.obelisk.examples.minimal";
android.displayName = "Obelisk Minimal Example";
ios.bundleIdentifier = "systems.obsidian.obelisk.examples.minimal";
ios.bundleName = "Obelisk Minimal Example";
__closureCompilerOptimizationLevel = null; # 클로저 컴파일러가 뭐지?
# 구글에서 만든 JS 컴파일러라 한다. 최적화 수준을 지정. null이면 안한다는 뜻
# 아마도 Reflex 코드를 GHCJS가 뽑아내면 그 다음 이게 돌지 않을까?
# 프로젝트에서 사용할 Nixpkgs에 없는 하스켈 패키지를 추가할 때
# 주로 pkgs.haskellPackages.callCabal2nix와 같이 cabal파일을 이용하여 패키지를 불러오는
# 함수를 사용한다. Cabal 패키지 설정을 Nix 표현식으로 변환하여 Nix가 하스켈 프로젝트의
# 의존성을 관리하도록 한다. .cabal 파일에서 의존성, 빌드 옵션, 소스 파일들을 분석해서
# Nix 표현식을 생성, Nix가 패키지를 빌드한다.
packages = {
mmark = hackGet ./dep/mmark; # 소스 코드를 지정한 곳(로컬, 인터넷 주소...)에서 가져 온다.
# hackGet : 원래 있던 건 아니고, obelisk에서 추가 했다.
# 하스켈 패키지를 가져오는데 쓴다.
};
# 위는 nixpkg에 없는 패키지를 추가하고,
# 아래는 nixpkg에 있는 패키지를 덮어 씌운다.
# 그런데 위아래 모두 mmark를 가지고 있다. TODO: overrides에만 있으면 되는 것 아닌가?
# 주로 pkgs.haskell.lib.overrideCabal과 같은 함수를 사용하여 cabal 패키지 설정을 오버라이드
overrides = self: super: {
temporary = dontCheck super.temporary; # dontCheck : 테스트 단계에서 checkPhase를 비활성화
email-validate = dontCheck super.email-validate;
mmark = doJailbreak (dontCheck (if (self.ghc.isGhcjs or false)
then dontHaddock super.mmark else super.mmark));
## doJailbreak : 하스켈 패키지 의존성 제약을 무시, 버전 범위 지정을 무시
modern-uri = doJailbreak (dontCheck (self.callHackageDirect {
pkg = "modern-uri";
ver = "0.3.4.4";
sha256 = "19qwfrida77mxix4v41qyvanfxy8qljg4fxz9ccxlcjdbks5v6pa";
} {}));
lucid = dontCheck (self.callHackageDirect {
pkg = "lucid";
ver = "2.11.1";
sha256 = "035idsv283s6484wyb8yndzw4rbyivvz6b3cwxan2cf0xd3s78dk";
} {});
markdown-unlit = self.callHackageDirect {
pkg = "markdown-unlit";
ver = "0.5.1";
sha256 = "194g4f12xpvdp9yr16ngxx6w9kmpvilkivvag3drpc8cv15csp8l";
} {};
# super.frontend는 원래의 frontend 패키지 정의
# overrideCabal 기존 cabal 패키지의 속성을 수정할 때 사용하는 닉스 함수
# drv : 기존 패키지(super.frontend)의 속성
# drv.buildTools 속성을 수정하여 새로운 빌드 추가
# 닉스가 빌드할 때 buildTools에 있는 것들을 PATH에 추가한다.
# 실제 실행시키는 곳은 Setup.hs 또는 *.cabal 설정에서 실행시킨다.
# (drv: { }) 기존 패키지 속성을 변경하는 람다 함수.
# 인자로 받은 derivation의 buildTools란 속성을 바꾸고 있다.
frontend = overrideCabal super.frontend (drv: {
buildTools = (drv.buildTools or []) ++ [ self.buildHaskellPackages.markdown-unlit ];
});
};
})
※ with obelisk;
라 하면, 바로 다음 하나의 표현식 스코프안으로 obelisk
가 가진 것들을 가져 온다.
nix flake init --template templates#haskell-nix --impure
# `--impure` is required by `builtins.currentSystem`
nix develop cabal build
nix flake show
nix build .#hello:exe:hello
nix run .#hello:exe:hello
기존 하스켈 프로젝트에 haskell.nix
지원을 추가하는 간편한 방법을 제공하는 명령줄 도구
hix init
명령은 flake.nix
와 nix/hix.nix
파일을 추가한다. 이렇게 하면, 프로젝트를 보통의 nix 도구들로 작업할 수 있게 된다.
cabal unpack hello
cd hello-1.0.0.2
nix run "github:input-output-hk/haskell.nix#hix" -- init
nix develop cabal build
nix flake show
nix build .#hello:exe:hello
nix run .#hello:exe:hello
다른 Hix 특징들을 쓰려면 Hix를 설치한다.
nix-env -iA hix -f https://github.com/input-output-hk/haskell.nix/tarball/master
최신 버전을 쓰려면
hix update
hix develop
, hix flake
, hix build
, hix run
등, nix
를 쓰듯 쓰는 게 인상적이다. 분명 nix flake에 익숙하다면 장점이 될 것 같다.
이들 명령은 flake.nix
를 쓰지 않고, 닉스 버전과 동일하게 작동한다는데, 무슨 말이지? hix는 flake를 쓰는 것 아니었나?
프로젝트 루트에 flake.nix
파일을 만들지 않아도, flake.nix
파일이 있을 때 nix
명령을 사용하듯 hix
를 사용할 수 있다는 뜻이다. hix는 .hix-flake/flake.nix
에서 자동으로 flake를 관리하고, 사용자는 신경 쓸 필요 없다. 직접 flake.nix
파일을 만들고 유지하고 싶지 않을 때 쓰기 좋다. (반면, haskell-flake는 프로젝트 루트에 flake.nix
파일을 직접 만들고 유지해야 한다.)
hix는 haskell.nix
스타일을 쓰면서, 필요할 때 .hix-flake/flake.nix
내부에서 flakes를 관리하며, flake와 비슷한 인터페이스를 제공한다.
Hackage에 등록되어 있는 모든 하스켈 패키지를 위한 빌드 지침instruction을 배포 중이다. 그런데, 닉스 패키지 검색을 하면, ghc
, cabal-install
, stack
말고는 나오는 게 없다. 하스켈 패키지는 너무 많아서 탑레벨 네임 스페이스에 등록되어 있지 않다. 탑레벨에 두면 검색이 너무 느려져서 haskellPackages
속성으로 분리되어 있다.
기본적인 설치
nix-env -f "<nixpkgs>" -iA haskellPackages.Allure ...
-f
(--file
): Nix 표현식 파일을 지정할 때. <nixpkgs>
는 시스템에 설치된 Nixpkgs 경로
-i
(--install
): 설치
-A
(--attr
): 속성을 통해 패키지를 지정. Allure
는, 패키지는 표현식으론 haskellPackages
의 속성이다. 이 옵션 없이는 <nixpkgs>
의 최상위 속성에서 해당 패키지를 찾는다.
“Nixpkgs 채널의 최신 버전에서 haskellPackages
속성 집합 안에서 Allure
속성을 찾아 설치하라”란 뜻이다.
haskell-
로 시작하는 패키지는 라이브러리(와 실행 파일)를 제공하고, 그렇지 않은 패키지는 실행 파일을 제공한다. 예를 들어, haskell-pandoc
은 라이브러리와 실행 애플리케이션을 같이 설치한다.
NixOS라면 -f "<nixpkgs>"
를 안쓰고, nixos.
을 붙여 nixos.haskellPackages
를 찾으라 하면 된다.
nix-env -qaP -A nixos.haskellPackages
-q
(--query
): 현재 사용할 수 있는 패키지 조회(아무 옵션 없이 쓰면, 현재 사용자 환경에 설치된 패키지에서만 조회)
-a
(--available
): 설치되지 않은 패키지도 포함해서, 사용할 수 있는 모든 패키지
-P
(--attr-path
): 패키지의 속성 경로도 출력. 예를 들어 lens
라 출력하지 않고, nixos.haskellPackages.lens
라 출력
-A
: 지정 속성만 조회. 예를 들어 -A nixos.haskellPackages
를 하면, haskellPackages
아래 모든 속성(패키지)을 나열
컴파일러와 해당 컴파일러로 컴파일한 패키지가 묶여 있다.
haskellPackages
는 haskell.packages.ghcXYZ
(XYZ
는 Nixpkg
에 있는 디폴트 버전)의 alias다.
하스켈 패키지는 일반적으로 Hackage에 있는 릴리즈를 기반으로 생성되지만, Hackage는 패키지 빌드를 위한 소스 코드도 제공하므로, 필요에 따라 소스 코드를 직접 다운 받아 패키지를 설치할 수도 있다.
-hledger-lib = (haskell.lib.overrideSrc haskellPackages.hledger-lib {
mysrc = /home/aengelen/dev/hledger/hledger-lib;
});
-hledger = (haskell.lib.overrideSrc haskellPackages.hledger {
mysrc = /home/aengelen/dev/hledger/hledger;
}).override {
hledger-lib = my-hledger-lib;
};
-web = haskell.lib.justStaticExecutables (
hledger(haskell.lib.overrideSrc haskellPackages.hledger-web {
src = /home/aengelen/dev/hledger/hledger-web;
})
{
.override hledger = my-hledger;
hledger-lib = my-hledger-lib;
});
overrideSrc { src, version } drv
: derivation drv
를 만드는데 쓰인 소스를 src
경로에 있는 것으로 바꾼다. version
속성은 옵션이다.
.override
: 패키지의 속성을 변경한다.
nix-env -f "<nixpkgs>" -iA haskellPackages.ghc haskellPackages.cabal-install
위 명령으론 설치된 바이너리들이 있는 폴더가 자동으로 $PATH
잡히지 않지만, 아래 명령은 자동으로 잡힌다.
nix-shell -p haskell.compiler.ghc784
Stack의 Nix 지원을 켜면, Stack이 내부적으로 nix-shell
을 이용한다.
nix-shell
로 만든 Nix 저장소 경로는 nix-shell
이 종료되면, “alive”가 아닌 상태가 되어 nix-collect-garbage
하면 사라져서 환경이 깨질 수 있다. 그럴 땐, nix-shell
에서 cabal-configure
를 다시 실행하면 된다.
nix-shell -p haskell.compiler.ghc784 --command "cabal configure"
configure phase는 GHC를 비롯한 모든 필요한 툴의 절대 경로(/nix/store
아래 경로)를 dist/
디렉토리 안의 빌드 설정에 기록한다. dist/
디렉토리는 빌드 과정에서 생성되는 다양한 파일들(설정 파일, 컴파일된 오브젝트 파일, 라이브러리, 실행 파일)이 저장된다. 절대 경로가 저장되는 파일은, dist/setup-config
, dist/build/*
등에 키-값 쌍같은 구조로 저장된다.
기본적으로 GHC는 자신의 lib
디렉토리에 모든 라이브러리가 설치될 것이라 가정한다. 이 방식은 전통적인 Unix 계열 시스템에선 문제 없지만, 닉스에선 작동하지 않는다. 닉스에서 GHC의 저장 경로는 한 번 빌드되면 불변이다. 추가로 라이브러리를 해당 경로에 넣을 수 없다. GHC는 자신의 코어 라이브러리 base
, containers
, Cabal
등을 제외한 다른 라이브러리는 어디있는지 모르게 된다. GHC가 추가 라이브러리들이 어디 있는지 알게 하려면 특별 빌드 함수인 ghcWithPackages
함수로 빌드해야 한다. 이 함수는 “하스켈 패키지의 속성 집합을 패키지 리스트에 매핑하는 함수”를 인자로 받는다. 예를 들어 ghcWithPackages (pkgs: [pkgs.mtl])
표현식은 mtl
라이브러리를 가진 GHC 복제 패키지를 생성한다. GHC 복제는 기존에 코어만 가지고 있던 GHC 패키지들을 alias로 가지고 있다.
새로운 프로젝트마다, 프로젝트가 필요한 라이브러리들이 추가된 GHC복제 패키지를 각 각 가지고 있다.
현재 nix-shell에 등록된 패키지를 보려면
[nix-shell:~]$ ghc-pkg list mtl /nix/store/zy79...-ghc-7.10.2/lib/ghc-7.10.2/package.conf.d: mtl-2.2.1
ghcWithPackages
함수로 override해서, 사용자들은 자신만의 원하는 환경을 만들 수있다. 다음 코드 조각snippet을 ~/.config/nixpkgs/config.nix
에 추가하고,
{
packageOverrides = super : let self = super.pkgs; in
{
myHaskellEnv = self.haskell.packages.ghc7102.ghcWithPackages
(haskellPackages: with haskellPackages; [
# 라이브러리
arrows async cgi criterion# 도구
cabal-install haskintex]);
}
}
# 위 코드는 ghcWithPackages (pkgs: [pkgs.mtl]) 모양 그대로이다.
nix-env -f "<nixpkgs>" -iA myHaskellEnv
로 컴파일러와 환경을 같이 준비할 수 있다.
위 표현식으로 닉스가 생성한 ghc
프로그램은, 사실 새로운 lib
디렉토리를 쓰도록 지정한 실제 GHC의 래핑 스크립트다.
> cat $(type -p ghc)
#! /nix/sotre/xlxj...-bash-4.3-p33/bin/bash -e
export NIX_GHC=/nix/store/19sm...-ghc-7.10.2/bin/ghc
export NIX_GHCPKG=/nix/sotre/19sm...-ghc-7.10.2/bin/ghc-pkg
export NIX_GHC_DOCDIR=/nix/store/19sm...-ghc-7.10.2/share/doc/ghc/html
export NIX_GHC_LIBDIR=/nix/store/19sm...-ghc-7.10.2/lib/ghc-7.10.2 exec /nix/store/j50p...-ghc-7.10.2/bin/ghc "-B$NIX_GHC_LIBDIR" "$@"
※ type -p
type 명령어
는 명령어가 어떻게 해석되는지(alias, 함수, 내장 명령어, 파일 …) 알려 준다. -p
를 주면 파일 경로만 출력한다.
※ -B
GHC의 라이브러리 경로를 지정, $@
쉘스크립트에 전달 된 모든 인수를 뜻한다.
위에 ghcWithPackages
를 ghcWithHoogle
로 바꾸면, 라이브러리들의 문서haddock도 같이 설치하고, 위에 ghc
스크립트처럼, 새로 설치한 문서 위치를 인식하게 설정하는 hoogle
래핑 스크립트를 만든다.
{
packageOverrides = super: let self = super.pkgs; in
{
myHaskellEnv = self.haskellPackages.ghcWithHoogle
(haskellPackages: with haskellPackages;[
# 라이브러리
arrows async cgi criterion# 도구
cabal-install haskintex]);
};
}
Hls는 현재 프로젝트가 쓰는 GHC버전으로 컴파일되어 있어야 한다.
pkgs.haskell-language-server.override { supportedGhcVersions = [ "884" "901" ] }
haskell-language-server-wrapper
를 실행하면, cabal이나 stack을 보고, 현재 프로젝트에 쓰이는 버전을 찾게 되고, path에 잡혀 있는 바이너리 중 적합한 걸 고른다.
> nix-shell [nix-shell] > code .
파일을 편집하고 싶을 때마다 nix-shell 실행해야 하는 건 번거롭다. direnv
로 해결할 수 있다.
--nix
플래그를 주면, 내부에서 닉스를 쓴다.
> git clone --recurse-submodules https://github.com/yesodweb/wai.git
> cd wai > stack --nix build
기본으로 닉스를 쓰게 하려면 stack.yaml
에 다음을 추가한다.
nix:
enable: true
packages: [pkgconfig zeromq zlib]
이렇게 하면, 모든 stack 명령어는 Nixpkgs에 있는 pkgconfig
, zeromq
, zlib
를 쓸 수 있는 환경에서 실행된다.
좀 더 복잡한 요구 사항이 있을 땐, shell
필드를 이용할 수 있다.
nix:
enable: true
shell-file: shell.nix
mkDerivation
을 래핑하고 있는 haskell.lib.buildStackProject
함수를 써서 derivation을 만든다.
{nixpkgs ? import <nixpkgs> { }, ghc ? nixpkgs.ghc }:
with nixpkgs;
let R = pkgs.R.override { enableStrictBarrier = true; };
in
{
haskell.lib.buildStackProject name = "HaskellR";
buildInputs = [ R zeromq zlib ];
inherit ghc;
}
ad hoc한 개발 환경을 만드는 가장 편한 방법은, 적절한 GHC 환경을 명령줄에서 써주면 된다.
nix-shell -p "haskellPackages.ghcWithPackages (pkgs: with pkg; [mtl pandoc])"
환경이 복잡하면 shell.nix
를 쓴다.
{ nixpkgs ? import <nixpkg> {}, compiler ? "ghc7102" }:
let
inherit (nixpkgs) pkgs;
ghc = pkgs.haskell.packages.${compiler}.ghcWithPackages (ps: with ps; [
monad-par mtl]);
in
{
pkgs.stdenv.mkDerivation name = "my-haskell-env-0";
buildInputs = [ ghc ];
shellHook = "eval $(egrep ^export ${ghc}/bin/ghc)";
}
다른 버전의 컴파일러 쓰려면
nix-shell --argstr compiler ghc784
Hackage에 있는 Lens를 해킹한다고 가정하면
> cabal get lens-4.11 && cd lens-4.11
Downloading lens-4.11...
Unpacking to lens-4.11/
> nix-shell "<nixpkgs>" -A haskellPackages.lens.env [nix-shell:/tmp/lens-4.11] >
cabal-install
도 nix-shell 환경이 제공하는 것이 아니니, 설치하고 $PATH
에 잡아 주어야 한다.
cabal2nix
를 쓰면 Cabal로 빌드하는 프로젝트를 자동으로 Nix로 빌드할 수 있게 바꿀 수 있다. nix-env -i cabal2nix
#### stand-alone 프로젝트 빌드
현재 프로젝트 foo
의 루트 디렉토리에서
cabal2nix . > foo.nix
그리고, default.nix
에 다음 코드를 추가한다.
{ nixpkgs ? import <nixpkgs> {}, compiler ? "ghc7102" }:
{compiler}.callPackage ./foo.nix { } nixpkgs.pkgs.haskell.packages.$
개발 환경을 위해 shell.nix
에 다음을 추가한다.
{ nixpkgs ? import <nixpkgs> {}, compiler ? "ghc7102" }:
(import ./default.nix { inherit nixpkgs compiler; }).env
위 파일들 준비가 끝나면, nix-build
로 빌드하고, Nix 저장소에 설치할 수 있다. 로컬 디렉토리에는 이를 가리키는 result
라는 심볼릭 링크 디렉토리가 생긴다. 또한, nix-shell로 들어가면, 필요한 GHC 버전, 라이브러리, 시스템 레벨의 라이브러리등이 잡힌 상태의 환경에서 cabal configure
, cabal build
를 쓰며 개발할 수도 있다.
만일, 시스템 레벨의 라이브러리가 전혀 필요하지 않으면, 위에서 준비한 .nix
파일들 없이
nix-shell --command "cabal configure"
를 실행하는 것으로 준비가 끝난다. 위 명령어를 실행하면, cabal-install
패키지가 빌드에 필요한 모든 자원들의 절대 경로를 찾아 dist/
디렉토리 안의 설정 파일들에 기록한다. 이 후, nix-shell 환경이 아닐 때 처럼 cabal build
같은 명령들을 실행할 수 있다.
default.nix, shell.nix를 직접 만들지 않고, 기본적인 내용으로 준비하려면
cabal2nix --shell . > shell.nix nix-shell --command "cabal configure"
이 후, nix-build shell.nix
를 실행해서 빌드 할 수 있다.
각 프로젝트의 cabal 정보를 읽어 default.nix
를 준비하고,
cd ~/src/foo && cabal2nix . > default.nix cd ~/src/bar && cabal2nix . > default.nix
디폴트 하스켈 패키지 집합에 빌드 결과물을 등록한다.
~/.config/nixpkgs/config.nix
{
packageOverrides = super:
{
haskellPackages = super.haskellPackages.override {
overrides = self: super: {
foo = self.callPackage ../src/foo {};
bar = self.callPackage ../src/bar {};
};
};
};
}
이제 nix-env -f "<nixpkgs>" -qA haskellPackages
하면, Hackage의 다른 패키지처럼 foo
와 bar
를 볼 수 있고, 다음처럼 빌드할 수 있다.
nix-build "<nixpkgs>" -A haskellPackages.foo
개발 환경에 들어가려면,
nix-shell "<nixpkgs>" -A haskellPackages.bar.env
srid/haskell-flake
flake-parts 모듈로 셋업한다. Cabal만 지원한다.(Stack은 지원하지 않는 것 같다.) 프로젝트 탑레벨에 .cabal
이나 cabal.xproject
가 있어야만 한다.
> mkdir example && cd ./example > nix flake init -t github:srid/haskell-flake#example
아래는 nixos.asia 페이지를 정리한 노트이다.
haskellPackages
callCabal2nix
cabal2nix
이 .cabal
파일을 읽어들여 “닉스 derivation을 생성하는 닉스 표현식”을 만들고, 이 닉스 표현식을 평가해서 derivation을 만든다. cabal2nix
가 만든 닉스 표현식에는 mkDerivation
함수나, mkDrivation
을 특정 목적으로 래핑한 함수가 들어가 있다.
※ 뜬금없이 예시를 따라하다 nix repl
멀티라인에서 막혔다.
원문 예시 중 repl
에서 멀티라인 입력이 필요한 부분이 있는데, 원문을 따라하면 입력이 되지 않는다. repl은 구문이 끝났는지, 안 끝났는지 추론해서 결정한다. 예전엔 extend
함수로 끝난 첫 줄이, 아직 덜 끝난 걸로 인식했었나 본데, 지금은 그렇지 않다. extend
뒤에 아직 구문이 끝나지 않았다는 표시로 (
를 써준다.
nix-repl> myHaskellPackages = pkgs.haskellPackages.extend
(self: super: {
shower = self.callCabal2nix "shower"
(pkgs.fetchgit {
url = "https://github.com/monadfix/shower.git";
rev = "2d71ea1";
sha256 = "sha256-vEck97PptccrMX47uFGjoBVSe4sQqNEsclZOYfEMTns=";
}) {}; })
extend
함수가 인자로 self: super: { ... }
람다 함수를 받고 있다. super
에는 확장되기 이전의 패키지 집합이 들어가고, self
는 확장된 이후의 패키지 집합이 들어간다. (아마도 이렇게 되려면 fix 패턴을 쓰고 있겠지?)
super
: pkgs.haskellPackages
의 기존 상태를 참조한다. Nixpkgs
에 정의되어 있는 하스켈 패키지 ghc
,base
,lens
,text
, …등과 하스켈 관련 유틸리티 함수 callCabal2nix
, callHackage
등이 포함된다.self
: super
에 extend
를 적용한 새로운 하스켈 패키지 집합Overlay
haskellPackages
에 있는 패키지를 덮어 씌우거나override, 추가할 때
nix develop
을 실행하면, flake.nix
의 outputs
에 있는 devShell
설정을 지침으로 삼아 쉘을 준비한다.
빌드가 아니라 개발이 목표일 때.
어차피 두 경우 모두 필요한 라이브러리는 같은게 아닐까 싶지만, 개발 쉘에는 ghcid
같은 도구나, 테스트 프레임워크, LSP서버, 디버깅 도구 같은 것들이 추가로 필요하다.
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
};
outputs = { self, nixpkgs }:
let
system = "aarch64-darwin";
pkgs = nixpkgs.legacyPackages.${system};
overlay = final: prev: {
todo-app = final.callCabal2nix "todo-app" ./. { };
};
myHaskellPackages = pkgs.haskellPackages.extend.overlay;
in
{
devShells.${system}.default = myHaskellPackages.shellFor {
packages = p : [
p.todo-app]:
with myHaskellPackages; [
nativeBuildInputs =
ghcid
cabal-install]
}
}
}
mkShell 함수 언어와 무관하게 쓴다.
shellFor 하스켈만을 위한 함수. mkShell
위에 만든 abstraction이라 한다.
모든 하스켈 패키지는 shellFor
를 가지고 있다. Haskell package로 구성된configured GHC 패키지셋을 가진 devShell
을 반환한다.
두 개의 인자를 받는다.
packages
: 로컬 하스켈 패키지를 의미한다.
nativeBuildInputs
: devShell
의 PATH
에서 사용 가능하도록 제공할 프로그램을 의미, 개발에 필요한 도구들을 지정한다.
전역 시스템을 건드리지 않고, 현재 프로젝트 환경에서만 쓸 외부 의존성을 준비해보자.
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
};
outputs = { self, nixpkgs }:
let
system = "aarch64-darwin";
pkgs = nixpkgs.legacyPackages.${system};
in
{
apps.${system}.postgress = {
type = "app";
program =
let
script = pkgs.writeShellApplication {
name = "pg_start";
runtimeInputs = [ pkgs.postgresql ];
text =
''
# 현재 프로젝트 dir를 저장소로 데이터베이스 초기화
[ ! -d "./data/db" ] && initdb --no-locale -D ./data/db
postgres -D ./data/db -k "$PWD"/data
'';
};
in "${script}/bin/pg_start";
};
};
}
nix run
으로 실행할 앱을 정의하고 있다. 여기서 앱은 Postgres 서버를 시작하는 간단한 쉘스크립트다. nixpkgs
는 이런 스크립트를 위한 writeShellApplication
이란 편리한 함수를 제공한다. ${script}
는 nix/store
에서 앱이 저장된 위치를 뜻한다.
※ pkg-config
라이브러리에 접근하는 표준 인터페이스 제공한다.
.pc
파일은 보통 /usr/lib/pkgconfig
또는 /usr/lib64/pkgconfig
에 설치 된다. 만일 패키지가 .pc
파일을 생성하는 설정이 없다면, 수동으로 만들어 등록할 수도 있다.
prefix=/usr
libdir=${prefix}/lib
includedir=${prefix}/include
Name: webkit2gtk-4.0 (필수)
Description: WebKitGTK library (선택)
Version: 2.46.6 (필수)
Libs: -L${libdir} -lwebkit2gtk-4.0 (필수) Cflags: -I${includedir}/webkitgtk-4.0 (필수)
잘 설치 되었는지 확인
> pkg-config --modversion webkit2gtk-4.0
> pkg-config --libs webkit2gtk-4.0 > pkg-config --cflags webkit2gtk-4.0
※ WebkitGTK
Webkit 렌더링 엔진을 GTK(그래픽 툴킷)와 통합한 라이브러리로, 주로 리눅스에서 웹브라우저나 웹컨텐체를 쓰는 애플리케이션을 개발하는데 쓰인다. GTK 버전에 따른 변형이 있다.
How to develop with Haskell and Nix
하스켈 섹션
Nixpkgs의 하스켈 인프라는 첫 째, 하스켈 컴파일러와 빌드 도구, 그리고 하스켈 패키지를 만들기 위한 패키징 인프라를 제공하고, 둘 째, 빌드된prebuilt 하스켈 라이브러리를 가지고 있는 하스켈 개발 환경을 제공하는 것을 목표로 한다.
컴파일러와 대부분의 빌드 툴은 탑레벨에 위치한다.
cabal-install
, stack
, hpack
, …nix
, cachix
haskellPackages : Hackage에 있는 모든 패키지들 자체가 아니라, 패키지를 빌드할 수 있는 명세derivation를 생성할 수 있는 닉스 표현식을 가지고 있다.
haskellPackages에 있는 속성attribute 이름은 항상 Hackage에 있는 같은 이름과 대응된다. Hackage에는 Nix에서 쓰지 않는 글자(+
,.
,…)도 이름으로 쓸 수 있으니, 완전히 같지 않을 수 있다.
Stackage에 있는 패키지일 경우, LTS스냅샷에 있는 버전을 디폴트 버전으로 갖고 있다.
만일, defaultGhcVersion = "ghc94"
라면, haskellPackages = haskell.packages.ghc94
와 같다. haskellPackages
는 디폴트 버전의 심볼릭 링크다.
haskellPackages.foo
라 하면, 디폴트로 Hackage에 있는 것 중 최신 버전 foo
패키지를 뜻한다. Nixpkgs는 고정된 Hackage 스냅샷으로, 스냅샷을 뜰 시점의 최신 패키지다.
haskell.packages.lts_22_11
처럼 Stackage 스냅샷을 지정해서, 해당 스냅샷에 들어있는 버전을 디폴트로 쓸 수 있다.
haskellPackages.foo_x_y_z
명시적인 버전 지정 x.y.z
를, 닉스는 이름으로 .
을 쓸 수 없으니, 언더바_
로 바꾼다.
cabal-install
을 써서 하스켈 패키지를 빌드할 때, 의존성을 푸는데, Hackage에 있는 모든 버전을 확인한 후, 조건에 맞는 적절한 버전을 선택하려는 시도를 한다. 반면, Nixpkgs는 이런 시도 없이, 단순히 입력값으로 패키지 목록을 받아서, 이들이 조건을 충족하는지만 본다. (버전 제약 조건을 우회하라면 jailbreak
를 쓴다.)
haskellPackages.mkDerivation
은 stdenv.mkDerivation
의 래퍼다.
stdenv.mkDerivation
은 닉스의 패키지 빌드를 위한 derivation을 생성하는 기본 함수다. 이 함수는 패키지 빌드, 설치, 테스트등의 단계를 정의한다. haskellPackages.mkDerivation
은 이 기본 함수가 가진 configurePhase
, buildPhase
, installPhase
이외에 하스켈에 특화된 단계들을 가지고 있다. Cabal 라이브러리를 앱을 통하지 않고 직접 사용하여 의존성을 해결하기 때문에 cabal-install
의 바이너리는 쓰이지 않는다.
하스켈 프로젝트에선 Setup.hs
파일을 통해 빌드, 테스트, 설치등의 작업을 정의한다. haskellPackages.mkDerivation
은 이를 직접 컴파일하고 실행해서 Haskell 프로젝트를 빌드한다.