닉스로 하스켈 프로젝트 빌드 (스케치 중)

Posted on March 7, 2025

아래 페이지들을 보며 스케치 중인데, 아직은 틀리게 해석, 이해한 곳도 많이 있는 것 같습니다. 교차하며 보며 조금씩 수정하고 있습니다. 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에서 닉스 소스 코드를 확인할 수 있다.

nixpkgs에 있는 하스켈 언어 프레임 워크

derviation을 생성하고, 개발쉘을 준비하는 기본적인 닉스 코드 모양은 아래와 같다.

let
  pkgs = import <nixpkgs> { };
in
{
  package = pkgs.stdenv.mkDerivation { ... };
  shell = pkgs.mkShell { ... };
}

mkDerivation, mkShell을 하스켈 패키지와 쉽게 쓸 수 있도록, 이들을 래핑한 함수들과 특별한 구조들을 만들어 둔, 하스켈 프레임워크가 준비되어 있다. 하나의 파일이, 하나의 derivation을 생성하는 것이 아니라, derivation은 패키지 단위로 준비한다. 만일, 하스켈 프로젝트가 비하스켈 의존성이 있다면, mkDerivationmkShell로 준비하고, 이 것들을 하스켈 프레임워크의 래핑 함수들이 가져다 쓴다.

하스켈 언어 프레임 워크

빠르게 보기

> nix-env -i cabal2nix cabal-install
> cabal2nix . > default.nix
> cabal2nix --shell . > shell.nix
> nix-shell --command 'cabal configure'

cabal2nix 프로젝트 .cabal 파일에서 의존성 정보를 가져오고, nixpkgs가 가진 haskellPackages 컬렉션에서 원하는 것들을 가져오며 의존성을 풀어갑니다.

nixpkgs 바로 아래 있는 툴

하스켈만을 위한 툴 ghc,cabal-install, stack, hpack, niv, cachix, …
위 툴들은 특별히 설치하지 않아도 nixpkgs 탑레벨에서 제공 된다.

pkgs.haskell.lib 아래 있는 라이브러리

jailbreaking : 패키지들의 모든 버전 제약을 날린다. 각 패키지 집합은, 각 의존성의 단 하나의 버전만 포함하니, 어차피 안전한 작업이다. 안전한 이유는, 닉스의 패키지 집합에는 여러 버전이 공존하지 않고, 오직 하나의 버전만 들어 있다. 예를 들어, pkgs.haskllPackagetext 패키지가 있다면, 그 안에는 어차피 text-2.0.2같은 하나의 버전만 존재한다. 이렇기 때문에 버전을 제거해도 달리지는 게 없다는 뜻이다. 보통 cabal에서는 패키지마다 의존 버전 범위를 지정하지만, Nix 패키지에서는 의미 없다.

derivation의 속성 중 jailbreaktrue로 놓으면, 버전을 제거한다.

How to develop with Haskell and Nix

NixOS Wiki
얼마나 복잡한 하스켈 프로젝트냐에 따라 선택할 수 있는 다양한 방법이 있다. 서브 프로젝트가 없으면 developPackage, 서브 프로젝트가 있고 여러 팀이 작업하면 haskell.nix, 서브 프로젝트가 있고 단일팀이면 shellFor.

스크립트 실행을 이용

#!/usr/bin/env nix-shell
#!nix-shell --pure -i runghc -p "haskellPackages.ghcWithPackages (pkgs: [ pkgs.turtle ])"

main = do
  putStrLn "Hello World"

닉스 캐싱/재현성 없이 직접 cabal 사용

닉스를 쓰지 않을 때처럼 cabal을 쓸 수 있다.

$ nix-shell -p "haskellPackages.ghcWithPackages (pkgs: with pkgs; [ cabal-install ])"
$ cabal init
$ cabal run

다른 라이브러리 의존성이 있을 땐, nix-shell의 옵션으로 추가할 수 있다.

Stack 사용

닉스 안에서 stack을 이용해서 프로그램을 패키징하고 싶으면, stack build를 부르는 stdenv.mkDerivation의 래퍼인 haskell.lib.buildStackProject을 쓴다. stack이 필요한 것들을 다운로드 받아야 하기 때문에 sandbox를 비활성화한다. nix-build --option sandbox false. 아래 예시는 R, zeromq, zlib를 쓰는 프로젝트를 stack으로 빌드한다.

with (import <nixpkgs> { });
haskell.lib.buildStackProject { # buildStackProject가 stack build를 부른다.
  name = "HaskellR";
  buildInputs = [ R zeromq zlib ];
}

developPackage 사용

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 필드를 읽어 들인다. 닉스 저장소에 있는 하스켈 패키지 중 깨진 것들이 있다. 닉스는 라이브러리의 최신 버전만 가지고 있어, 구버전을 필요로 하는 패키지들은 깨진다. 이럴 때는 overridessource-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: 
    pkgs.haskell.lib.addBuildTools drv (with pkgs.haskellPackages;
      [ cabal-install
        ghcid
      ]);
}

srid.ca/haskell-nix

복수 패키지일 때는 shellFor

shellFordevelopPackage와 비슷한데, 복수의 패키지를 동시(이 건 cabal.project와 비슷)에 개발할 수 있게 조금 더 복잡한 기능을 가지고 있다. developPackage와는 다르게 shellFor는 derivation을 output으로 가지고 있지 않다.

사용자 프로젝트를 “추가 하스켈 패키지”로 하스켈 패키지에 추가하기 위해, 하스켈 패키지 집합을 extend 하거나 override한다. (예를 들어, haskellPackages.extendpackageSourceOverrides를 사용하여, 프로젝트의 경로만 제공하면 이를 컴파일할 수 있다.) 그 후, 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 ...를 쓴다.

Overrides

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에 아래 둘 중 하나를 넘긴다.

all-cabal-hashes

derivation의 속성을 바꾸기 위해, 더 강력한 override 시스템을 쓸 수도 있다.

pkgs.haskellPackages.developPackage {
  root = ./.;
  overrides = self: super: {
    random = pkgs.haskell.lib.overrideCabal super.random {
      version = "1.1";
      sha256 = "sha256-...";
      doCheck = false;
    }
  }
}

developPackagesource-overridesoverrides 속성 차이는 다음과 같다.
source-overrides
패키지 소스 코드를 덮어 씌울 때 쓴다. 주로 버전 충돌을 해결하거나 로컬에서 수정된 패키지를 사용할 때.

  source-overrides = {
    myPackage = /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은 구 패키지의 속성을 수정한 새 패키지를 반환한다. 이 함수가 수정 가는한 옵션은 여기 소스를 참고한다.

Super-Simple Haskell Development with Nix

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-shell에서 실행되는 하스켈 스크립트 만들기

※ 별다른 옵션 없이 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: 패키지를 지정하는 옵션인데, ghcWithPackagespandoc을 포함한 새로운 패키지를 생성하고, 그 패키지를 불러온다는 의미다

-i: 명령어를 nix-shell 환경에서 실행한다.

※ 지금은 하스켈 관련 유틸리티를 쓰기 편하도록 gw라는 이름의 래핑 유틸리티가 생겼다.

nix-shell 모드는 두 가지가 있다.

Practical Nix Flakes

Serokell 블로그 - 실용 닉스 플레이크

※ 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

haskell.nix

haskell.nix (첫 번째 커밋 날짜 2018-04-06 11:21:33 +0800) Cabal 프로젝트나 Stack 프로젝트를 Nix 코드로 자동 번역한다. NixpkgshaskellPackages를 쓰지 않는, 독립적인 패키지 관리시스템으로 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 파일을 만든다.

Niv
닉스 프로젝트 의존성을 트래킹하는 명령줄 도구다. flake를 쓰려면 안봐도 된다고 한다. 일단 패스.

haskell.nix는 stack.yamlcabal.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>'.
    haskellNix.sources.nixpkgs-unstable
    # These arguments passed to nixpkgs, include some patches and also
    # the haskell.nix functionality itself as an overlay.
    haskellNix.nixpkgsArgs;
in 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;
project ./. ({ pkgs, hackGet, ... }:
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가 가진 것들을 가져 온다.

flake-y nix 에서 haskell.nix 사용하는 방법

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

Hix

기존 하스켈 프로젝트에 haskell.nix 지원을 추가하는 간편한 방법을 제공하는 명령줄 도구
hix init 명령은 flake.nixnix/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와 비슷한 인터페이스를 제공한다.

Haskell Support for Nix

haskell4nix

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아래 모든 속성(패키지)을 나열

컴파일러와 해당 컴파일러로 컴파일한 패키지가 묶여 있다.
haskellPackageshaskell.packages.ghcXYZ(XYZNixpkg에 있는 디폴트 버전)의 alias다.

하스켈 패키지는 일반적으로 Hackage에 있는 릴리즈를 기반으로 생성되지만, Hackage는 패키지 빌드를 위한 소스 코드도 제공하므로, 필요에 따라 소스 코드를 직접 다운 받아 패키지를 설치할 수도 있다.

패키지 브랜치 설치하기

my-hledger-lib = (haskell.lib.overrideSrc haskellPackages.hledger-lib {
  src = /home/aengelen/dev/hledger/hledger-lib;
});
my-hledger = (haskell.lib.overrideSrc haskellPackages.hledger {
  src = /home/aengelen/dev/hledger/hledger;
}).override {
  hledger-lib = my-hledger-lib;
};
hledger-web = haskell.lib.justStaticExecutables (
    (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의 라이브러리 경로를 지정, $@ 쉘스크립트에 전달 된 모든 인수를 뜻한다.

컴파일러와 라이브러리, 그리고 후글, 문서 인덱스 설치

위에 ghcWithPackagesghcWithHoogle로 바꾸면, 라이브러리들의 문서haddock도 같이 설치하고, 위에 ghc 스크립트처럼, 새로 설치한 문서 위치를 인식하게 설정하는 hoogle 래핑 스크립트를 만든다.

{
  packageOverrides = super: let self = super.pkgs; in
  {
    myHaskellEnv = self.haskellPackages.ghcWithHoogle
                     (haskellPackages: with haskellPackages;[
                       # 라이브러리
                       arrows async cgi criterion
                       # 도구
                       cabal-install haskintex
                     ]);
  };
}

haskell-language-server 설치

Hls는 현재 프로젝트가 쓰는 GHC버전으로 컴파일되어 있어야 한다.

pkgs.haskell-language-server.override { supportedGhcVersions = [ "884" "901" ] }

haskell-language-server-wrapper를 실행하면, cabal이나 stack을 보고, 현재 프로젝트에 쓰이는 버전을 찾게 되고, path에 잡혀 있는 바이너리 중 적합한 걸 고른다.

nix-shell에서 haskell-language-server가 GHC 찾기

> nix-shell
[nix-shell] > code .

파일을 편집하고 싶을 때마다 nix-shell 실행해야 하는 건 번거롭다. direnv로 해결할 수 있다.

stack으로 하스켈 프로젝트 빌드

--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;
}

nix-shell을 위한 ad hoc 환경

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" }:
nixpkgs.pkgs.haskell.packages.${compiler}.callPackage ./foo.nix { }

개발 환경을 위해 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의 다른 패키지처럼 foobar를 볼 수 있고, 다음처럼 빌드할 수 있다.

nix-build "<nixpkgs>" -A haskellPackages.foo

개발 환경에 들어가려면,

nix-shell "<nixpkgs>" -A haskellPackages.bar.env

haskell-flake

srid/haskell-flake
flake-parts 모듈로 셋업한다. Cabal만 지원한다.(Stack은 지원하지 않는 것 같다.) 프로젝트 탑레벨에 .cabal이나 cabal.xproject가 있어야만 한다.

> mkdir example && cd ./example
> nix flake init -t github:srid/haskell-flake#example

하스켈 프로젝트 닉스화Nixifying

아래는 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 패턴을 쓰고 있겠지?)

Overlay
haskellPackages에 있는 패키지를 덮어 씌우거나override, 추가할 때

devShell 닉스화

nix develop을 실행하면, flake.nixoutputs에 있는 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
        ]:
        nativeBuildInputs = with myHaskellPackages; [
          ghcid
          cabal-install
        ]
      }
    }
}

mkShell 함수 언어와 무관하게 쓴다.
shellFor 하스켈만을 위한 함수. mkShell위에 만든 abstraction이라 한다.
모든 하스켈 패키지는 shellFor를 가지고 있다. Haskell package로 구성된configured GHC 패키지셋을 가진 devShell을 반환한다.

두 개의 인자를 받는다.
packages : 로컬 하스켈 패키지를 의미한다.
nativeBuildInputs : devShellPATH에서 사용 가능하도록 제공할 프로그램을 의미, 개발에 필요한 도구들을 지정한다.

외부 의존성 닉스화

전역 시스템을 건드리지 않고, 현재 프로젝트 환경에서만 쓸 외부 의존성을 준비해보자.

{
  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 공식 문서 하스켈 섹션

하스켈 섹션
Nixpkgs의 하스켈 인프라는 첫 째, 하스켈 컴파일러빌드 도구, 그리고 하스켈 패키지를 만들기 위한 패키징 인프라를 제공하고, 둘 째, 빌드된prebuilt 하스켈 라이브러리를 가지고 있는 하스켈 개발 환경을 제공하는 것을 목표로 한다.

가용한 패키지

컴파일러와 대부분의 빌드 툴은 탑레벨에 위치한다.

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.mkDerivationstdenv.mkDerivation의 래퍼다.

stdenv.mkDerivation은 닉스의 패키지 빌드를 위한 derivation을 생성하는 기본 함수다. 이 함수는 패키지 빌드, 설치, 테스트등의 단계를 정의한다. haskellPackages.mkDerivation은 이 기본 함수가 가진 configurePhase, buildPhase, installPhase 이외에 하스켈에 특화된 단계들을 가지고 있다. Cabal 라이브러리를 앱을 통하지 않고 직접 사용하여 의존성을 해결하기 때문에 cabal-install의 바이너리는 쓰이지 않는다.

하스켈 프로젝트에선 Setup.hs 파일을 통해 빌드, 테스트, 설치등의 작업을 정의한다. haskellPackages.mkDerivation은 이를 직접 컴파일하고 실행해서 Haskell 프로젝트를 빌드한다.

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