Nix学習備忘録Part 1: Nixに入門する

2022-01-19T23:22:36Z

1 はじめに

技術記事をまともに書くのは初めてなので、 記述が冗長(既にこの章が冗長かもしれない)であると感じることもあるかもしれませんが、 温かい目で読んで頂ければ幸いです。 また、読者にNixの入門をさせることが目的ではなく、 あくまで個人的にNixを勉強していて、 そろそろどこかにまとめないと忘れそうだと感じたので、 本シリーズはタイトル通り``備忘録"という位置付けです。 もし、情報に誤り等御座いましたら、 Twitter、GitHub、またはEmailがフッターにありますので、 そちらからご報告頂ければ有難いです。

2 Nixとは

``Nix"は純粋関数型パッケージマネージャの1つです。 パッケージを純粋関数型言語のとして扱います。 つまり、各パッケージは副作用を持たない関数によって構築され、 ビルド後に変更されることはありません。

…と、あまり抽象的な話をし過ぎてもHaskell等の関数型プログラミング言語を使ったことがある人であれば、 何となくは、こういうことかと理解できるかもしれませんが、 そうでない人にとっては何を言ってるかさっぱりだと思うので、 (というより、私自身も完全には理解できていないので、) 実際にPythonを例に具体的に環境構築を行なってみます。

Nixは学習コストが高いことでも有名なので、 全部理解して発信するより、機能に少しずつ触れながら、 こういうことできました~って発信する方が、 この記事を読む方も置いてけぼりにならなくて良いかなという 淡い期待と甘えで筆を進めています。

3 Nixのインストール方法

日本語でも既に記事があり、何番煎じかわかりませんが、 一応載せておきます。

※以下のインストール方法は2022/01/19現在のものです。

4 NixでPythonの環境を構築する

4.1 何ができるようになるか

今まで私はpyenvでPythonのバージョンを管理し、 virtualenvを使って各種パッケージの管理を行なっていました。

Nixを用いると、pyenvもvirtualenvも使わず、 Pythonのバージョン、パッケージをまとめて管理できます1。 また、この環境はプロジェクトごとに作ることができ、 グローバル環境を汚すことなく、 クリーンにインストールすることができます。

4.2 今回作るもの

何を作るのかはあまり重要じゃないのですが、 PNG形式の画像ファイルをPDF形式に変換するプログラムを作成します。 理由については、本筋と関係ないので、読みたい方だけ読んでください。

なぜPDF変換のプロジェクト?

論文を書いたりするときに、LaTeXを使うのですが、 pngファイルの出力が上手く行かないことがあり、 eps形式のファイルが推奨はされているのですが、 pdfで十分出力される上、使い勝手も良いので、 変換する作業が必要になることがちょくちょくあります。

論文に使うような画像の変換に、 オンラインの変換サービスを利用するのは流石に気が引けるので、 Pythonで一気に変換できた方が良いなと考えた次第です。

4.3 Pythonを使えるようにする

まず、現在の状況を確認します。 ~/work下にpng2pdfディレクトリを作成します。

ホームディレクトリ~/home/usernameにあるとします。

$ cd ~
$ mkdir work && cd $_
$ mkdir png2pdf && cd $_
$ pwd
/home/usename/work/png2pdf
$ ls
$

既存のPythonの環境はすべて消し去ったので、 今はpythonコマンドは使えない状況です:

$ python
python: command not found

ここで、お好みのエディタで現在のディレクトリ(~/work/png2pdf)に、 以下のようなshell.nixという名前のファイルを作成します。

{ pkgs ? import <nixpkgs> {} , ...}:

with pkgs;
mkShell {
  buildInputs = [ python3 ];
}

以下のような状態であればOKです。

$ pwd
/home/username/work/png2pdf
$ ls
shell.nix
$ cat shell.nix
{ pkgs ? import <nixpkgs> {} , ...}:

with pkgs;
mkShell {
  buildInputs = [ python3 ];
}

ここで、nix-shellというコマンドを入力すると、 Pythonがインストールされたシェルが起動します。

$ nix-shell
# 初回は若干時間が掛かるかもしれません。
[nix-shell:~/work/png2pdf]$ python
Python 3.9.9 (main, Nov 15 2021, 18:05:17)
[GCC 10.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> 1 + 1
2
>>> exit()
[nix-shell:~/work/png2pdf]$ 

~/work/png2pdf下でPythonが使えるようになりました!

すでにグローバルにPythonが入っていた人のために、 どのPythonが使われているのかも確認してみましょう。

[nix-shell:~/work/png2pdf]$ which python
/nix/store/dn4fwp0yx6nsa85cr20cwvdmg64xwmcy-python3-3.9.9/bin/python

頭に/nix/store/と付いています。 各種パッケージ等がインストールされる先はこのNix Storeと呼ばれる場所で、自分のPCで利用可能なパッケージがすべて含まれています。

また、python3-3.9.9というバージョンの前にハッシュのようなものが付いています。 このおかげで、バージョンは同じだけど、依存関係は違うようなPythonも衝突(conflict)しないことが保証されています。

このシェル環境から出るにはexitとタイプします。 この環境から抜けると、最初の状態と同様、pythonは使えなくなります。 グローバルの環境は一切汚れていないことがわかります。

[nix-shell:~/work/png2pdf]$ exit
$ python
python: command not found

今回作るPNGをPDFに変換するプログラムはimg2pdfというパッケージを必要とします。 しかし、現在そのようなパッケージはインストールしていないため、使うことができません。 実際にnix-shellコマンドで再度シェルに入ってみます。 2回目なので、すぐにシェルが起動します。

$ nix-shell
[nix-shell:~/work/png2pdf]$ python
Python 3.9.9 (main, Nov 15 2021, 18:05:17)
[GCC 10.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import img2pdf
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ModuleNotFoundError: No module named 'img2pdf'
>>> exit()
[nix-shell:~/work/png2pdf]$ exit
$

4.4 Pythonパッケージを利用可能にする

img2pdfを利用可能にするためには、shell.nixbuildInputspython3Packages.img2pdfという記述を追加します:

{ pkgs ? import <nixpkgs> {} , ...}:

with pkgs;
mkShell {
  buildInputs = [
    python3
    python3Packages.img2pdf # <- この行を追加
  ];
}

再度nix-shellコマンドにより、再度シェルを起動してみます。

これでimg2pdfが使えるようになってるはず…

$ nix-shell
[nix-shell:~/work/png2pdf]$ python
(省略)
>>> import img2pdf
Traceback (most recent call last):
(中略)
ModuleNotFoundError: No module named 'PIL'
>>>

PILという名前のモジュールが見つかりませんってエラーが出ました。 img2pdfを使用するには、pillowってパッケージをインストールする必要があるみたいです。

この問題どうにかならないの?(Pythonユーザー向け)
実はこの辺の依存関係もまとめて管理できるPythonのパッケージマネージャにPoetryというものが存在して、 これとNixのより進んだ技術を組み合わせるとこの辺りの依存関係問題も解決できます。

ということで、前置きが長くなりましたが、依存関係のpillowbuildInputに加えます。

{ pkgs ? import <nixpkgs> {} , ...}:

with pkgs;
mkShell {
  buildInputs = [
    python3
    python3Packages.pillow # <- この行を追加
    python3Packages.img2pdf
  ];
}

再度nix-shellを実行し、img2pdfが使えるか確認してみます。

$ nix-shell
[nix-shell:~/work/png2pdf]$ python
Python 3.9.9 (main, Nov 15 2021, 18:05:17)
[GCC 10.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import img2pdf
>>> img2pdf.__version__
'0.4.3'
>>> exit()
[nix-shell:~/work/png2pdf]$ 

無事img2pdfが使えるようになりました!

4.5 プログラムを書く(詳細は省略)

Pythonのプログラムは主目的から外れるので、 プログラムとファイル構造だけ載せ、プログラムに関する詳細な説明は省略します。

メインはNixの方なので、Pythonのこの書き方おかしいだろとか、 ファイル構造どうにかならんのかとか、 そういうツッコミは心の中でお願いします。笑

[nix-shell:~/work/png2pdf]$ mkdir png pdf
[nix-shell:~/work/png2pdf]$ ls
pdf  png  shell.nix

このpngフォルダにPDFに変換したいPNG形式の画像ファイルを入れます。 pdfフォルダは空のままです。

[nix-shell:~/work/png2pdf]$ ls png
fuga.png  hoge.png
[nix-shell:~/work/png2pdf]$ ls pdf
[nix-shell:~/work/png2pdf]$ 

お好みのエディタで、~/work/img2pdf/内に、 以下のようなmain.pyファイルを作成します:

import os
import img2pdf
from PIL import Image 

if __name__ == '__main__':
    cwd = os.getcwd()
    pdf_dir_path = f"{cwd}/pdf/"
    png_dir_path = f"{cwd}/png/"

    for png_filename in os.listdir(png_dir_path):
        pdf_filename = png_filename.replace('.png', '.pdf')

        # 既に変換済みファイルがあったらスキップ
        if pdf_filename in os.listdir(pdf_dir_path):
            continue

        with open(pdf_dir_path + pdf_filename, "wb") as f:
            f.write(img2pdf.convert(
                Image.open(png_dir_path + png_filename).filename
            ))

ファイル構造が以下のようになっていればOKです。

[nix-shell:~/work/png2pdf]$ tree .
.
├── main.py
├── pdf
├── png
│   ├── fuga.png
│   └── hoge.png
└── shell.nix

2 directories, 4 files

では、main.pyを実行してみます:

[nix-shell:~/work/png2pdf]$ python main.py
Image contains an alpha channel which will be stored as a separate soft mask (/SMask) image in PDF.
Image contains an alpha channel which will be stored as a separate soft mask (/SMask) image in PDF.

[nix-shell:~/work/png2pdf]$ tree .
.
├── main.py
├── pdf
│   ├── fuga.pdf
│   └── hoge.pdf
├── png
│   ├── fuga.png
│   └── hoge.png
└── shell.nix

2 directories, 6 files
[nix-shell:~/work/png2pdf]$ exit
$ 

新しくhoge.pdffuga.pdfpdfディレクトリの中に追加されました。

5 まとめ

Nixを用いてPythonと、Pythonのパッケージを利用可能にする方法を紹介しました。

次回は、毎回nix-shellで環境に入って、 exitで出るの面倒臭くない?って問題を解決します。


  1. 本当にNixだけで良いかというと、実はそうでもなく、 PoetryというPythonのパッケージマネージャ―を使った方が良くなったりもするのですが、 小規模のプロジェクトではそれほど問題にはならないので、 今後、必要に応じて記事を書きます。↩︎