클래식 닉스로 하스켈 빌드 예시

Posted on March 20, 2025

이 글은 클래식 (non flake) 닉스를 사용합니다.
아래 나온 소스는 lionhairdino/nix_sample 에서 다운로드 할 수 있습니다.

기본

default.nix는 빌드용 derivation을 만드는 방법을 가지고 있는 파일입니다. 이 파일이 있는 폴더에서 nix-build를 하면, 기본으로 default.nix파일을 읽어 들여 빌드 작업을 합니다. shell.nix는 개발쉘용 derivation을 만드는 방법을 가지고 있는 파일입니다. 이 파일이 있는 폴더에서 nix-shell을 하면, 기본으로 shell.nix파일을 읽어 들여 개발 쉘을 준비합니다. ※ 반드시 default.nix에서 빌드 설정만, shell.nix에서 개발쉘 설정만 하는 건 아닙니다만, 보통 두 파일을 이용합니다.

아래 derivation은 derivation을 만들어내는 가장 기본적인 함수입니다.

derivation { ... }

실제 작업에서는 이 함수를 바로 쓰지 않고, 다음 빌드용, 개발 쉘용으로 래핑한 함수들을 주로 쓰게 됩니다.

빌드용 derivation을 생성하는 래퍼
많은 경우, 이 함수를 써서 derivation을 생성합니다. buildPhase, installPhase 같은 기본적인 빌드 단계가 미리 짜여져 있는 프레임 워크입니다.

stdenv.mkDerivation { ... }

개발 쉘용 derivation을 생성하는 래퍼
주로 nix-shell이 읽어들이게 됩니다.

stdenv.mkShell{ ... }

이 함수들을 또 다시 하스켈에 쓰기 편하도록 래핑한 함수들이 있습니다.

하스켈 프로젝트 빌드용 derivation을 생성하는 래퍼
cabal 빌드와 관련된 설정을 자동으로 추가해 줍니다.

haskellPackages.mkDerivation { ... }

하스켈 프로젝트 개발 쉘용 derivation을 생성하는 래퍼
cabal이나 ghc가 필요한 경우 자동으로 포함합니다.
하스켈 프로젝트에서 nix-shell이 읽어들입니다.

haskellPackages.developPackage { ... }

래핑 계층을 보면, stdenv.mkShell이 내부에서 stdenv.mkDerivation을 쓰니, 다음과 같이 볼 수 있습니다.

derivation --> stdenv.mkDerivation --> haskellPackages.mkDerivation
                       |
                       └─-> stdenv.mkShell --> haskellPackages.developPackage

가장 간단한 샘플 빌드

> mkdir simplist
> cd simplist
> cabal init 
simplist
├── app
│   └── Main.hs
├── default.nix
└── simplist.cabal

위와 같이 한 후, 아래 파일들을 추가하거나, 수정합니다. (당장 필요없는 텍스트 메타 파일들은 뺐습니다.)

default.nix 빌드 설정

let
  pkgs = import <nixpkgs> { };
  #haskellPackages = pkgs.haskell.packages.ghc982;
  haskellPackages = pkgs.haskellPackages;
in
  haskellPackages.mkDerivation {
    pname = "simplist";
    version = "0.1.0.0";
    license = "license";
    src = ./.;
  }

shell.nix 개발 쉘 설정

let
  pkgs = import <nixpkgs> { };
in
  pkgs.haskellPackages.developPackage {
    root = ./.;
  }

app/Main.hs

{-# LANGUAGE OverloadedStrings #-}
module Main where

import Data.Text (intercalate)
main :: IO ()
main = print $ intercalate " " ["Haskell","and","Nix"]

simplist.cabal

cabal-version:      3.0
name:               simplist
version:            0.1.0.0
license:            MIT
license-file:       LICENSE
author:             lionhairdino
maintainer:         lionhairdino@gmail.com
category:           Development
build-type:         Simple
extra-doc-files:    CHANGELOG.md
common warnings
    ghc-options: -Wall

executable simplist
    import:           warnings
    main-is:          Main.hs
    build-depends:    base >=4.18.0.0
                    , text
    hs-source-dirs:   app
    default-language: Haskell2010

base >=4.18.0.04.18.0.0이상이면 다 허용, base ^>=4.184.18.*.*들만 허용

.cabal에는 text 라이브러리 의존성이 있는데, default.nix에는 이와 관련된 설정이 전혀 없어도 빌드 됩니다. 이유는, haskellPackages.developPackage.cabal 파일의 build-depends를 읽고, 필요한 라이브러리를 자동으로 추가합니다. pkgs.haskellPackages는 GHC 버전에 맞는 패키지 집합을 가지고 있고, 이 집합에 text 패키지가 이미 있기 때문에 별도로 추가하는 설정이 없어도 됩니다.

외부 라이브러리 의존 샘플 빌드

외부 라이브러리가 필요한 예시를 보기 위해 zlib를 쓰도록 바꿨습니다.

app/Main.hs

{-# LANGUAGE ForeignFunctionInterface #-}
module Main where

import Foreign.C.String

foreign import ccall "zlibVersion" zlibVersion :: IO CString

main :: IO ()
main = do
  ver <- zlibVersion
  verStr <- peekCString ver
  putStrLn ("zlib version: " ++ verStr)
lionhairdino/workroom/simplist_zlib via λ 9.8.3 ➜ cabal build
Build profile: -w ghc-9.8.3 -O1
In order, the following will be built (use -v for more details):
 - simplist-0.1.0.0 (exe:simplist) (file app/Main.hs changed)
...
error: undefined reference to 'zlibVersion'
...
collect2: error: ld returned 1 exit status
ghc-9.8.3: `cc' failed in phase `Linker'. (Exit code: 1)
Error: cabal: Failed to build exe:simplist from simplist-0.1.0.0.

undefined reference to 'zlibVersion'zlib 시스템 라이브러리를 못 찾는다는 에러입니다. 하스켈 패키지에 있는 라이브러리였다면 별도 추가 설정이 없어도 .cabal에 의존성을 추가하는 것만으로 됐겠지만, zlib는 하스켈 패키지에 있는 라이브러리가 아닙니다.

수작업으로 zlib설치해서 해결해도 되지만, Nix가 알아서 설치하는 빌드 프로세스를 만들 수 있습니다.

simplist_zlib.cabalz 추가

executable simplist
    ...
    build-depends:    base >=4.18.0.0
    extra-libraries:  z

default.nix (아래 예시는 개발 쉘 설정은 없고, 빌드 설정만 있습니다.)

{ pkgs ? import <nixpkgs> { } }:
  pkgs.haskellPackages.mkDerivation {
    pname = "simplist_zlib";
    version = "0.1.0.0";
    src = ./.;
    license = "";
    configureFlags = [ "--extra-include-dirs=${pkgs.zlib.dev}/include"
                       "--extra-lib-dirs=${pkgs.zlib.dev}/lib"
                     ];
    extraLibraries = [ pkgs.zlib.dev ];
  }

하스켈 패키지에 없는 외부 라이브러리 의존성을 위해 extraLibraries 필드를 이용합니다.

default.nix는 함수를 반환해도 되고,

{ pkgs ... } :
{
  devShell = pkgs.haskellPackages.developPackage ...;
  package = pkgs.mkDerivation ...;
}

다른 표현식을 반환해도 됩니다.

let
  pkgs = ...
in
  pkgs.haskellPackages.developPackage ...;

※ 닉스 언어에서 컬리 브라켓 { }속성 집합을 표현할 때도 쓰고, 스코프 블록을 지정할 때도 쓰입니다. 저는 처음 볼 때, 명시적으로 이렇게 설명한 자료를 보지 못해 좀 혼란스럽기도 했습니다. 드물긴 하지만, 빌드 설정과 개발 쉘 설정을 같이 정의하거나(보통은 default.nix, shell.nix로 나눠서 합니다) 빌드 타켓이 여러 개일 경우, 속성 집합을 반환하는 함수로 정의해야 합니다.

개발 쉘과 빌드 설정 같이

default.nix 이 번에는 개발 쉘과 빌드 설정을 같이 해보겠습니다.

{ pkgs ? import <nixpkgs> {} }:

let # 편의를 위한 바인딩들
  haskellPackages = pkgs.haskellPackages;
  ghc = haskellPackages.ghc;
  cabal-install = haskellPackages.cabal-install;
in
{
  # 환경 변수를 잡으려면 developPackage는 적당하지 않습니다.
  devShell = pkgs.mkShell {
    name = "simplist_zlib";
    buildInputs = [ pkgs.haskellPackages.ghc pkgs.haskellPackages.cabal-install pkgs.zlib.dev ];

    # shellHook에서 환경 변수를 설정
    shellHook = '' 
       export LD_LIBRARY_PATH="${pkgs.zlib.dev}/lib:$LD_LIBRARY_PATH"
    '';
  };
  package = haskellPackages.mkDerivation rec {
    pname = "simplist_zlib";
    version = "0.1.0.0";
    src = ./.;
    license = "";
    configureFlags = [ "--extra-include-dirs=${pkgs.zlib.dev}/include"
                       "--extra-lib-dirs=${pkgs.zlib.dev}/lib"
                     ];
    extraLibraries = [ pkgs.zlib.dev ];
  };
}  

nix-buildpackage 속성을 평가한 결과는 바이너리 simplist_zlibresult 폴더 아래 만들어내고, devShell 속성을 평가한 결과는 result-2란 디렉토리가 아닌 텍스트 파일을 만들어냅니다. devShell은 개발 환경 설정을 위한 항목으로, 개발 환경을 위한 환경 변수 설정 (declare -x AR="ar"같은 내용들)이 잔뜩 들어 있는 파일로, source 메타파일명으로 쉘스크립트 실행을 할 수 있긴 한데, 이런 식으로 언제 쓰이는지 아직 확인하지 못했습니다.

보통은 쉘 설정은 따로 shell.nix에 넣어 놓고, nix-shell로 개발쉘을 빌드하므로, 위와 같은 상황은 잘 만나지 않습니다.

※ 속성명 devShell, package가 특별한 의미를 가지는 건 아닙니다.

haskellPackages.mkDerivation
하스켈 패키지를 빌드하는데 필요한 속성 집합을 인자로 받고, derivation을 반환합니다.

{ isLibrary
, executableHaskellDepends
, pname
...
} : ...

내부적으로 pkgs/development/haskell-modules/generic-builder.nix를 씁니다.

일반 하스켈 빌더는 stdenv.mkDerivation의 래퍼입니다.

mkDerivation= makeOverridable mkDerivationImpl;
mkDerivationImpl= pkgs.callPackage ./generic-builder.nix {...}
callPackage= drv: args: callPackageWithScope defaultScope drv args;
callPackageWithScope= scope: fn: manualArgs: ...

Nixpkgs에 없는 외부 라이브러리를 쓸 때

haskellPackages.mkDerivation
pkg-config mudules

simplist_libfoo
├── app
│   └── Main.hs
├── default.nix
├── libfoo
│   ├── foo.c
│   ├── foo.h
│   └── Makefile
└── simplist-libfoo.cabal

c로 만든 외부 라이브러리리를 빌드 및 설치하고, 하스켈에서 이 라이브러리에 있는 함수를 가져다 쓰는 상황을 가정했습니다. Main.hs

{-# LANGUAGE ForeignFunctionInterface #-}
module Main where
import Foreign.C.String
foreign import ccall "hello" c_hello :: IO ()
main :: IO ()
main = do
  putStrLn "Hello Haskell"
  c_hello

foo.c

#include <stdio.h>
#include "foo.h"
void hello() {
  printf("Libfoo Hello\n");
}

foo.h

#ifndef FOO_H
#define FOO_H
void hello();
#endif

Makefile

CC = gcc
CFLAGS = -fPIC -shared
LIBNAME = libfoo.so
all:
	$(CC) $(CFLAGS) foo.c -o $(LIBNAME)
clean:
	rm -f $(LIBNAME)

simplist_libfoo.cabal

cabal-version:      3.0
name:               simplist-libfoo
version:            0.1.0.0
license:            MIT
license-file:       LICENSE
author:             lionhairdino
maintainer:         lionhairdino@gmail.com
build-type:         Simple

common warnings
    ghc-options: -Wall

executable simplist-libfoo
    main-is:            Main.hs
    build-depends:      base >=4.18.0.0
    hs-source-dirs:     app
    default-language:   Haskell2010
    pkgconfig-depends:  foo
    extra-libraries:    foo

build-depends: base >=4.18.0.0, foo 이렇게 해야 될 것 같지만, foo는 하스켈 라이브러리가 아니라서, 여기에 추가하지 않고, extra-libraries에 추가해햐 됩니다.
foo라이브러리 위치를 GHC한테 알려줘야 하는데, 닉스 저장소는 해시를 포함하고 있어 경로를 수동으로 지정하려면 까다롭습니다. pkg-config가 알아서 경로를 제공하도록 하기 위해, pkgconfig-depends를 씁니다.

default.nixshell.nix 파일을 만드는데, foo 라이브러리 빌드 및 설치는 두 파일에서 모두 필요로 하니, 따로 빼냅니다.

libfoo/libfoo.nix

{ pkgs ? import <nixpkgs> {} }:

let
  libfoo = pkgs.stdenv.mkDerivation rec {
    pname = "foo";
    version = "0.1";
    src = ./.;
    buildInputs = [ pkgs.gcc ];
    buildPhase = ''
      make
    '';
    # 닉스가 기본 동작으로 $out 디렉토리를 만들고, make install을 호출하는데,
    # installPhase를 지정하면, 기본 동작을 하지 않는다.
    installPhase = ''
      mkdir -p $out/lib $out/include
      cp libfoo.so $out/lib/
      cp foo.h $out/include
    '';
    meta = {
      description = "foo - C library";
      platforms = pkgs.lib.platforms.all;
    };
    # libfoo는 $out 풀경로를 가진다.
  };

  # pkg-config에 등록용 .pc 파일
  libfooPkgConfig = pkgs.writeTextDir "foo.pc" ''
    prefix = ${libfoo}
    includedir = ${libfoo}/include
    libdir = ${libfoo}/lib

    Name: foo
    Description: foo - C library
    Version: 0.1
    Cflags: -I${libfoo}/include
    Libs: -L${libfoo}/lib -lfoo
  '';
  # 
in {
  inherit libfoo libfooPkgConfig;
}

default.nix

{ pkgs ? import <nixpkgs> {} }:

let
  haskellPackages = pkgs.haskellPackages;
  ghc = haskellPackages.ghc;
  cabal-install = haskellPackages.cabal-install;

  libfooDefs = import ./libfoo/libfoo.nix { inherit pkgs; };
  libfoo = libfooDefs.libfoo;
  libfooPkgConfig = libfooDefs.libfooPkgConfig;

in {
  package = builtins.trace "${libfooPkgConfig}" haskellPackages.mkDerivation rec {
    pname = "simplist_libfoo";
    version = "0.1.0.0";
    src = ./.;
    license = "License";
    preCompileBuildDriver = ''
      export PKG_CONFIG_PATH=${libfooPkgConfig}:$PKG_CONFIG_PATH
    '';
    buildTools = [ pkgs.pkg-config ];
#    pkg-configDepends = [ libfoo ]; TODO 용도 확인하기
  };
}
> Error: Setup: The program 'pkg-config' version >=0.9.0 is required but it
> could not be found.

pkg-config가 없다는 오류로, nativeBuildInputs에 항목을 추가하는 속성인 buildTools를 추가한다.

buildTools = [ pkgs.pkg-config ];

shell.nix

{ pkgs ? import <nixpkgs> {} }:

let
  haskellPackages = pkgs.haskellPackages;
  ghc = haskellPackages.ghc;
  cabal-install = haskellPackages.cabal-install;

  libfooDefs = import ./libfoo/libfoo.nix { inherit pkgs; };
  libfoo = libfooDefs.libfoo;
  libfooPkgConfig = libfooDefs.libfooPkgConfig;

in {
  devShell = builtins.trace "${libfooPkgConfig}" pkgs.mkShell {
    name = "simplist_libfoo";
    buildInputs = [
      ghc
      cabal-install
#      libfoo
    ];
    nativeBuildInputs = with pkgs; [
      pkg-config
    ];

    shellHook = ''
      export PKG_CONFIG_PATH=${libfooPkgConfig}:$PKG_CONFIG_PATH
    '';
  };
}

writeTextDir Nix 저장소의 서브 디렉토리 안에 텍스트 파일을 만든다.
nixos.org/manual - writeTextDir
아래 둘은 동일하게 /nix/store/<store path>/share/my-file 파일을 만든다.

writeTextFile {
  name = "my-file";
  text = ''
    Contents of File
  '';
  destination = "/share/my-file";
}

writeTextDir "share/my-file"
  ''
  Contents of File
  ''

비슷한 함수로 writeScript는 파일을 만들고, 실행 속성을 줍니다. 조금씩 동작이 추가 되어 있는 writeScriptBin, writeShellScript, writeShellScriptBin 등의 함수들이 있는데, 이렇게 api를 늘려가는 게 좋은 방식인지는 모르겠습니다.

.cabalpkgconfig-depends 필드
Cabal은 pkg-config를 써서, 현재 패키지가 필요로 하는 시스템에 있는 라이브러리와 extra compilation, 링커 옵션을 찾는다. pkg-config --list-all 명령으로 가용한 라이브러리 목록을 볼 수 있다. 현재 프로젝트에서만 쓸 라이브리라면, 현재 프로젝트 폴더 아래에 라이브러리를 두고, pkg-config 파일인 .pc파일도 프로젝트 안에 만들어 두고, PKG_CONFIG_PATH 환경 변수를 이용할 수 있다.

NIX_STATE_DIR 환경 변수로 nix 데이터베이스 위치를 임시로 옮길 수 있다.


함수 인자 속성 집합 확인 방법

※ 프레임워크에 있는, 함수들이 받는 인자의 속성 집합이 궁금할 땐, 다음 방법으로 실제 코드를 확인 할 수 있습니다.

> nix repl
nix-repl> pkgs = import <nixpkgs> {}
nix-repl> :edit pkgs.haskellPackages.developPackage
  ...
  developPackage =
    { root
    , name ? lib.optionalString (builtins.typeOf root == "path") (builtins.baseNameOf root)
    , source-overrides ? {}
    , overrides ? self: super: {}
    , modifier ? drv: drv
    , returnShellEnv ? pkgs.lib.inNixShell
    , withHoogle ? returnShellEnv
    , cabal2nixOptions ? "" }:
  ...

혹은 인자의 속성 집합만 볼 수도 있습니다.

nix-repl> pkgs = import <nixpkgs> {}
nix-repl> builtins.functionArgs pkgs.haskellPackages.developPackage
{
  cabal2nixOptions = true;
  modifier = true;
  name = true;
  overrides = true;
  returnShellEnv = true;
  root = false; <-------- false: 필수 속성이란 뜻
  source-overrides = true;
  withHoogle = true;
}

mkDerivation처럼 고정된 인자 목록이 있는 게 아니라, 속성 집합을 받을 경우는 작동하지 않습니다.

디버깅 - 바인딩된 값 표시

in {
  devShell = builtins.trace "${libfooPkgConfig}" pkgs.mkShell {
    ...
    shellHook = ''
      export PKG_CONFIG_PATH=${libfooPkgConfig}:$PKG_CONFIG_PATH
    '';

devShell =에서 쓰이고 있는 ${libfooPkgConfig}가 궁금하면, builtins.trace 메시지 작업 형식으로 써서, 바인딩 값을 확인할 수 있다.

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