Envar — ディレクトリーごとに切り替える環境変数を一元管理する

Envar というコマンドラインツールを作りました。

github.com

これは環境変数の値をディレクトリーごとに切り替えるツールです。

例えば FOO という環境変数について、ディレクトリー path/to/A では hoge という値にして、ディレクトリー path/to/B では fuga という値にする、ということができます。下記のような vars.yaml ファイルでその設定をします。

FOO:
  path/to/A: hoge
  path/to/B: fuga

また、環境変数の値を別のコマンドの出力値にするということもできます。

この例は自分が使っている設定ファイルから抜粋したもので、gh コマンドで使用するアカウントを私用・しごとで切り替えるものです。

# vars.yaml
GH_TOKEN:
  /home/kazuki/Projects/Work:
    gh: kakkun61_work
  /home/kazuki/Projects:
    gh: kakkun61
# execs.yaml
gh: gh auth token --user %s

よくあるかもしれない質問

direnv と何が違うの?

direnv はそのディレクトリーで設定したい環境変数をそのディレクトリーに置いた設定ファイルに書きます。主にプロジェクトを使う人みんなに必要になる環境変数を設定する用途です。プロジェクト都合の環境変数です。

それに対して envar は設定ファイルはユーザーの設定ディレクトリーに置きます(例えば ~/.config/envar)。上記例のアカウントの切り替えなど個人の都合の環境変数を設定する想定です。

対応しているシェルは?

今のところ bash だけです。需要があれば zsh とかも対応するかも。

飛行機の中で Haskell プロジェクトをビルドする

いや別に飛行機の中でビルドするのが主題なわけではないのですが、オフラインモードのことを機内モードと言いますからね。最近の飛行機は Wi-Fi の提供があったりするらしいですが。

さて、あなたの Haskell プロジェクトをオフラインモードでビルドすることができますか? まあ、今時オフライン環境も珍しいですし「そんな必要あるのか?」という感覚もあるかもしれません。Nix ではビルド再現性のためにオフライン環境でビルドできることが求められます1

Nix で Haskell プロジェクトをビルドするには Input Output(iohk.io)の作成した haskell.nix が使用されるのが普通です。

github.com

これは cabal ファイルや stack ファイル、もしくは plan ファイルを入力とし、Cabal のパッケージひとつにつきひとつの Nix デリベーションに翻訳するシステムです2。Cabal がやってることを Nix(と Haskell)で再実装したと言ってもいいんじゃないかと思います。

いやあ、これが「結構大掛かりなシステムだなあ」と思うわけですよね。「もうちょっと cabal-install に頼ってビルドできないかなあ」と前々から思っていて、とりあえず Proof of Concept が動いたので紹介しようと思います。

コンセプト

自分のアイデアの肝腎は Cabal の「ローカルリポジトリー」を使うことです。普通はローカルリポジトリーを使うことはありません。Hackage つまりリモートリポジトリーを使いますから。ですが、Cabal は Hackage 以外のリポジトリーも使えるように作られています。一般的には ~/.config/cabal/config にある設定ファイルには下記のように書かれています3

repository hackage.haskell.org
    url: http://hackage.haskell.org/ 

このファイルは、cabal update の実行時にこのファイルがなければデフォルトの内容で生成されます。そのデフォルトの内容に Hackage の URL が記載されているので、みんなは Hackage からパッケージを取ってこられます。なので、やろうと思えば社内リポジトリーを立てて社内ライブラリーを配布したりすることもできます(認証は秘密鍵でできるっぽい)4。そして、この URL の部分は file+nocache というスキーマの URL も書くことができます5

repository my-local-repository
    url: file+noindex:///absolute/path/to/directory

/absolute/path/to/directoryディレクトリーにはパッケージの sdist ファイルを配置します。

/absolute/path/to/directory/
    foo-0.1.0.0.tar.gz
    bar-0.2.0.0.tar.gz

ここまで準備できれば後は簡単で、下記ステップでオフラインビルドができるようになります。

  1. ローカルリポジトリーのパスを書いた config ファイルを用意する
  2. 依存ライブラリーの sdist ファイルをローカルリポジトリーに配置する
  3. 環境変数コマンドライン引数で、用意した config ファイルを参照するようにする
  4. cabal build する

ツール

さて「ここまで準備できれば後は簡単」と書きましたが、依存ライブラリーの sdist を取得するはちょっと邪魔くさいです。間接依存や依存間のバージョン制約もあります。なので plan ファイルを元に sdist を取得するちょっとしたスクリプトを用意しました。

github.com

dependencies=$(cabal-plan topo --hide-builtin | grep -v ' ')for d in $dependencies
do
  url=https://hackage.haskell.org/package/$d/$d.tar.gz
  if [ $verbose -eq 0 ]
  then
    wget --quiet "$url"
  else
    wget --no-verbose "$url"
  fi
done

cabal-plan で依存のリストを取得して Hackage から wget でダウンロードするようにしています。

あとは flake.nixbuildPhase が肝腎なので気になる人は見ておくといいと思います6

github.com

runHook preBuild

# `cabal build` writes a file at a local repository,
# and so it must be writable.
cp -r --no-preserve=all $src/.local-repository .
# This cabal.config file declares using the local repository.
export CABAL_CONFIG=$src/cabal.config
# Set a writable directory for cabal
export CABAL_DIR=$TMPDIR/cabal
cabal="cabal --project-dir=$src --builddir=$TMPDIR --verbose"
$cabal v2-build --only-dependencies all
$cabal v2-build all

runHook postBuild

まだ確認できていないこと

cabal.project ファイルに source-repository-package が記載されていたらどうなるのか確認できていません。もしかすると cabal.project ファイルも切り替えないといけないかもしれないです。cabal.project.local に何か記載すれば source-repository-package を無効化できたりしないかなあ。誰か教えてください。


  1. 厳密に言うと Nix の用意する環境の中がオフラインで、Nix の処理系自体はネットワークで情報を取ってこれる。これは「Haskell の言語の中では副作用禁止だが、Haskell 処理系は副作用できる」みたいな話と似ているように思う。
  2. もしかしたらデリベーションは、コンポーネントつまり lib とか exe とか test とかと対応するのかも。
  3. ファイルの場所は環境や環境変数で変わる。詳しくはセクション 4.1.3. Directories に記載がある。
  4. 詳しくはセクション 4.1.4. Repository specification に記載がある。
  5. 詳しくはセクション 4.1.4.2. Local no-index repositories に記載がある。
  6. 記事を書いていたら「If the directory is not writable, you can append #shared-cache fragment to the URI, then the cache will be stored inside the remote-repo-cache directory.」というのを見つけたので、ローカルリポジトリーをコピーするステップはなくすことができそう。

Shake のキャッシュが効いてなかった

下記の記事を覚えていましょうか?

kakkun61.hatenablog.com

Shake を使って静的ウェブサイト生成するようにしたことを書いた記事です。

ただ、どうも生成が遅いんです。しょっちゅうは更新しないので「まあいいか」と放置していたのですが、最近手を付けることがあったので改善してみました。

fsatrace はどう追跡してくれるのか

前方型1の Shake アクションの場合、「fsatrace で変更を追跡する」というようなことがリファレンスに書いてあり、「よく分からんけどそうなんか」ぐらいに思って使っていました。

hackage.haskell.org

実は fsatrace は動的リンクライブラリーのロード時にトレース付きのラッパーを噛ますツールで、外部実行バイナリーを実行したときにしか依存の追跡をしてくれないということが分かりました2

つまり、今回の場合は主たる処理を Hint で行なっているため、ほぼキャッシュがされていなかったのです!

そこで、何をキャッシュするべきかを明示しないといけません。

キャッシュするよう Shake に伝える

Shake に「これキャッシュしておいて」と指示するために cacheActionWith を使用しました。

cacheActionWith
  :: ( Typeable a, Binary a, Show a
     , Typeable b, Binary b, Show b
     , Typeable c, Binary c, Show c
     )
  => a -> b -> Action c -> Action c 

a が呼び出し箇所ごとに異なるキーの型で、b はその値がキャッシュ時と同じならキャッシュ済みの c の値を返す、c はキャッシュされる結果の型です。

今回は下記のようにしました。

lucid source destination param = do
  …
  hsContents <- Shake.forP hsFiles Shake.readFile' -- hsFiles はインタープリター内で読むファイル
  let hash = Shake.hash (hsContents, param) -- インタープリターで実行する処理の入力をまとめてハッシュ値を得る
  result <- Shake.cacheActionWith ("hint: " ++ source) hash $ Hint.runInterpreter $ do

hsContentsparam のどちらも変更されなければキャッシュされた値が result になります。

(今書いていて思ったけどハッシュ値を取らなくても cacheActionWith の第2引数にペアのタプルを渡せばいいかも。)

また、キャッシュの都合、result の型が Binaryインスタンスでなければならず、元々は関数を内部に持つ HtmlT だったため単純にはインスタンスにできず、インタープリター内で show までしてしまうことで対処しました。

結果、初回ビルドは

Build completed in 1m27s

無変更で2度目は

Build completed in 0.85s

やったね🎉


  1. Makefile のようなビルドツールが後方型。
  2. なので Go 製実行バイナリーだと追跡できない。

Kensington Slimblade にトラックボールを

Kensington Slimblade を12年ぐらい使っているんですが、最近掃除してもシリコーンオイルつけても転がりが悪くなってきました。なのでいっちょベアリング仕様にするかと改造しました。その紹介です。


分解は裏の滑り止めを剥がしてねじを7つぐらいはずします。他も目に付き次第はずします。

ベアリングはエレコムのものを流用します。

元々の人工ルビーがあるところにいい感じに穴を開けます。マスキングテープをベアリングパーツの寸法に切り出して貼ってけがいて振動カッターで小さめに開けてやすりでぴったりの寸法まで広げました。

動作確認をしつつボールとセンサーのクリアランスを調整しセッチャコ。

スルスルグッドになりました!

スクロール操作はベアリングの回転と直交するので他の方向より転がりにくいけどまあいいでしょう。

蛇足

メッキパーツが親指の摩擦で禿げたので修復しようとプラモデルのメッキ落としに漬けてたら ABS は不可で恢復不能になりました。

MDF 板をレーザーカットしてペンキで塗ってなんとか取り繕いました。

ヨシ!

知らない天井だ

初めて入院しました。

経緯としてはこんな感じです。

技術書典から帰ってきた次の日の火曜日の昼間から、めまいがしだしました。

どんなめまいかというと頭を振ると脳が遅れて回る感じのめまいです。

「まあ一晩寝れば治るだろう」と様子を見てたんですが治らず。

水曜日も治らず、ゆっくりながら外を歩いたら気分の悪さからじっとり冷や汗が出る感じです。

木曜日にさすがに病院に行くかと耳鼻科に行こうと外に出るも、押し車を押すお年寄りよりも遅い速さでしか歩けず、しかも途中で音を吐き引き返しました。

「これはさすがに」ということで妻が救急安心センターおおさかに電話をしてくれ、「脳神経外科に」ということで義父に車を出してもらい病院に行きました。

車の振動がもうつらすぎて何度か嘔吐し、病院に着いた頃には全身の服が冷や汗でぐっしょりでした。

病院では総合内科で診察と検査をしてもらうことになり、いくつか耳鳴り・難聴の確認と運動テスト(?)の後、心電図と血液検査とレントゲン撮影と MRI 検査をしてもらいました。

さいわい、めまいの原因となる重い病気が見つかることはなく、良性発作性頭位めまい症ということになるみたいです。

気分が悪くあまり食べられてなかったこともあり、栄養と抗めまい薬の点滴をしてもらっています。

それが最初の写真でした。

病院で点滴をしてもらいながら一晩寝て、今はトイレに行くくらいなら少ししかめまいを感じないぐらいになっています。

(6月9日追記)6月8日に無事退院しました。

最近は Cosence(Scrapbox)に書いて満足することが多いという話

ブログの更新が少ない。

最近は Cosence(Scrapbox)に書いて満足することが多い。

scrapbox.io

各ページも「これで完成」というより気が向いたり新たな知見が得られたりしたときに書き足している感じになっている。

Nix User Repository に自作パッケージを追加する

みなさん Nix は使用していますか?

シェル環境を非破壊的にセットアップできて便利ですよね。

Nix はそれだけでなく、自作ソフトウェアのビルドツールとして利用でき、パッケージの作成と配布の手段としても利用できます。

Nix のパッケージリポジトリーとしてはみなさんご存じ Nixpkgs があります。

search.nixos.org

でも、使われるか分からない自作ソフトウェアを Nixpkgs に公開するのはちょっと気が引けるわけです。

そういうときのために Nix User Repository(NUR)があります。

nur.nix-community.org

どういうパッケージを NUR に登録するとよいかはドキュメントに次のように書いてあります。

  • Packages that are only interesting for a small audience
  • Pre-releases
  • Old versions of packages that are no longer in Nixpkgs, but needed for legacy reason (i.e. old versions of GCC/LLVM)
  • Automatic generated package sets (i.e. generate packages sets from PyPi or CPAN)
  • Software with opinionated patches
  • Experiments

ここまでは、「パッケージを公開したいけど Nixpkgs に公開するほどでもない」という側面から NUR の存在意義を見ました。

逆に次のように思う人もいるはずです。

pkgs.fetchzip や(Flakes なら)inputs で任意のパッケージを取ってこれるんだからわざわざ NUR に登録しなくてもいいのでは?」

確かにそれはそうで、その上で NUR に登録する利点としては NUR のウェブ UI で検索ができるのでユーザーが存在に気付きやすくなるなどです。

Package search for NUR

NUR を使うとき

一旦 NUR を使うときはどう使うのかを見ていきましょう。

次のコードは NUR のパッケージをシェルで使う場合の例で、NUR 公式ドキュメントからの引用です。

(1) の箇所で NUR を取得し、(2) の箇所でオーバーレイを追加し、(3) の箇所でパッケージ pkgs.nur.repos.mic92.hello-nur を参照しています。mic92リポジトリー名(通常はユーザー名と同じ)で hello-nur がパッケージ名です。

  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
    flake-utils.url = "github:numtide/flake-utils";
    nur = {                                               # (1)
      url = "github:nix-community/NUR";
      inputs.nixpkgs.follows = "nixpkgs";
    };
  };

  outputs = { self, nixpkgs, flake-utils, nur }:
    flake-utils.lib.eachDefaultSystem (system:
      let
        pkgs = import nixpkgs {
          inherit system;
          overlays = [ nur.overlay ];                     # (2)
        };
      in
      {
        devShells.default = pkgs.mkShell {
          packages = [ pkgs.nur.repos.mic92.hello-nur ];  # (3)
        };
      }
    );
}

パッケージのほかにも Nix ライブラリーや Nix OS モジュール・Nixpkgs オーバーレイが提供され(でき)ます。詳しくは公式ドキュメントを参照してください。

NUR のしくみ

NUR は NUR の Git リポジトリーで完結するわけではありません。NUR Git リポジトリーは個々人のリポジトリーへのリンクを repos.json に持ち定期的に個々人のリポジトリーをマージします。

{
    "repos": {
        ⋮
        "kakkun61": {
            "github-contact": "kakkun61",
            "url": "https://github.com/kakkun61/nur-packages"
        },
        ⋮
}

個々人のリポジトリーでは default.nixlib modules overlays それからパッケージのデリベーションを提供します。

{ pkgs ? import <nixpkgs> { } }:
{
  lib = import ./lib { inherit pkgs; }; # functions
  modules = import ./modules; # NixOS modules
  overlays = import ./overlays; # nixpkgs overlays

  wd = pkgs.callPackage ./pkgs/wd { };
}

自作パッケージの追加のしかた

まずは前述の「個々人のリポジトリー」を自分用に作成します。リポジトリーのテンプレートが用意されているのでそれを利用します。

github.com

そして自作ソフトウェアのパッケージを default.nix で提供できるようにします。そのとき、default.nixpkgs を引数に取ること・builtins.fetchTarball ではなく pkgs.fetchzip を使うこと・デリベーションは broken = true; に設定すること、などルールがあるのでドキュメントやテンプレートのコメントを読みましょう。 自分の場合は次のようなファイルを pkgs/wd/default.nix に用意しました。

{ pkgs, fetchzip }:

let
  version = "80e0814140c33201adf0e5213c426fb7bd5f47cf";
  project =
    fetchzip {
      url = "https://github.com/kakkun61/wd/tarball/${version}";
      sha256 = "sha256-usZTBF9pk0IsGdZb6IqEHBEaBX7mnXutykpm9TJHAWk=";
      extension = "tar.gz";
    };
in
(import "${project}/linux" { inherit pkgs; }).default.overrideAttrs {
  broken = true;
}

そしてルートの default.nix でこのファイルを読んでいます。

そしてこれらがちゃんと評価実行ができるか確認します。

ここまでできたら NUR に自分のリポジトリーを登録する PR を出します。PR タイトルのフォーマットが指定されているだけで特に概要に追記することなどはありませんでした。自分の PR はこれです。

github.com

PR がマージされれば NUR が取り込んでくれるのを待ちます。すぐに更新してほしい場合は HTTP エンドポイントがあるので通知します。テンプレートの GitHub Actions に実装されているのでしたい人はセットアップしましょう。

自分のページが NUR のウェブサイトに表示されましたか?おめでとうございます!