이 글은 클래식 (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 ()
= print $ intercalate " " ["Haskell","and","Nix"] main
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.0
은 4.18.0.0
이상이면 다 허용, base ^>=4.18
는 4.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
import ccall "zlibVersion" zlibVersion :: IO CString
foreign
main :: IO ()
= do
main <- zlibVersion
ver <- peekCString ver
verStr 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.cabal에 z
추가
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-build
로 package
속성을 평가한 결과는 바이너리 simplist_zlib
을 result
폴더 아래 만들어내고, 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: ...
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
import ccall "hello" c_hello :: IO ()
foreign main :: IO ()
= do
main putStrLn "Hello Haskell"
c_hello
foo.c
#include <stdio.h>
#include "foo.h"
void hello() {
("Libfoo Hello\n");
printf}
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:
$(LIBNAME) rm -f
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.nix
와 shell.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
를 추가한다.
[ pkgs.pkg-config ]; buildTools =
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";
}
"share/my-file"
writeTextDir ''
Contents of File
''
비슷한 함수로 writeScript
는 파일을 만들고, 실행 속성을 줍니다. 조금씩 동작이 추가 되어 있는 writeScriptBin
, writeShellScript
, writeShellScriptBin
등의 함수들이 있는데, 이렇게 api를 늘려가는 게 좋은 방식인지는 모르겠습니다.
※ .cabal
의 pkgconfig-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 메시지 작업
형식으로 써서, 바인딩 값을 확인할 수 있다.