器楽的緩怠

fuzzy note次郎

Nix Flakes を使って Rust プロジェクトの環境を用意する

Nix で Rust の環境を用意してみたのでそれについて書きます。 筆者はNix初心者なので参考にする場合は注意してください。

最終的に完成するものはこちらです:

github.com

使うもの

ステップバイステップでやってみる

Gitリポジトリを作成

まずはディレクトリを作成し、Gitリポジトリを作ります。

mkdir rust-nix-fenix-example
cd rust-nix-fenix-example
git init

利用する Rust ツールチェーンの定義

次に、利用する Rust ツールチェーンの定義をしましょう。rust-toolchain.toml というファイルを作成し、必要な設定をします。設定できる内容についてはrustupのドキュメントを参照してください。

今回はこのように記述しました:

# rust-toolchain.toml
[toolchain]
channel = "1.78.0"
components = ["rustc", "cargo"]
profile = "minimal"
targets = []

Flakes を書く

今のところリポジトリには rust-toolchain.toml しか存在しません。次はFlakesで環境を作りましょう。 flake.nix というファイルを作成し、次のように記述します。

Fenix を使って rust-toolchain.toml の内容を読み込み、環境を定義しています。

# flake.nix
{
  description = "Rust environment example";

  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
    flake-utils.url = "github:numtide/flake-utils";
    fenix = {
      url = "github:nix-community/fenix/monthly";
      inputs.nixpkgs.follows = "nixpkgs";
    };
  };

  outputs = { fenix, flake-utils, nixpkgs, ... }:
    flake-utils.lib.eachDefaultSystem (system:
      let
        toolchain = fenix.packages.${system}.fromToolchainFile {
          file = ./rust-toolchain.toml;
          sha256 = "sha256-opUgs6ckUQCyDxcB9Wy51pqhd0MPGHUVbwRKKPGiwZU=";
        };
        pkgs = nixpkgs.legacyPackages.${system};
      in
      {
        formatter = pkgs.nixpkgs-fmt;
        devShells.default = pkgs.stdenv.mkDerivation {
          name = "rust environment";
          nativeBuildInputs = [
            toolchain
          ];
        };
      }
    );
}

flake.nix を評価して有効化する

flake.nix と rust-toolchain.toml を Git tracked なファイルにしておきます。

git add .

direnv を使って環境を有効化する設定をしている場合は、use flake と書いた .envrcディレクトリに配置し、初回のみ direnv allow を実行します。

echo "use flake" > .envrc
direnv allow

direnv を含めた設定方法はこちらの記事に書きました: lemonadern.hatenablog.com

例示した rust-toolchain.toml の内容を変更した場合は、flake.nixのsha256付近を評価中に次のようなエラーが発生するでしょう。

       error: hash mismatch in fixed-output derivation '/nix/store/0iqd8n9gzj5lb3q9x1l9hyqk7dcg7pha-channel-rust-1.77.0.toml.drv':
         specified: sha256-opUgs6ckUQCyDxcB9Wy51pqhd0MPGHUVbwRKKPGiwZU=
            got:    sha256-+syqAd2kX8KVa8/U2gz3blIQTTsYYt3U63xBWaGOSc8=

flake.nix にて toolchain の sha256 として定義したものは依存先のファイル内容が同じであることを保証するためのハッシュなので、rust-toolchain.tomlを変更した場合はハッシュ値も変更する必要があります。 エラーで新しく提示されたハッシュ値got: で示されたほう)でflake.nixの記述を上書きしてください。

Cargo プロジェクトとしてセットアップする

Rustのツールチェーンはインストールできたので、あとはCargoのプロジェクトとしてセットアップするだけです。

cargo init

完成

このようなリポジトリができました:

github.com

おわりに

「NixでRust を使うときはなんか Overlay ってやつを使わないといけないらしい。ふんふん、Fenixとかrust-overlay ってのがあるのね、、、えっと、結局どう書けばいいの?」 となってしまったのでこの記事を書きました。

もっと洗練されたやりかたがあるかもとは思うんですが、あくまで1事例として、困っている人の助けになれば嬉しいです。マサカリをお持ちのかたはいい感じに投げてくれると嬉しいです。

参考

trap.jp

こちらの記事でツールチェーンの定義を rust-toolchain.toml に一本化できることを知り、大いに参考にさせてもらいました!助かりました!

Nixでいい感じのdevShellを使うまで

devShell とは

NixにはdevShellという便利な機能があります。devShellはプロジェクトで定義した依存を読み込んだシェル環境(?)のことで、これを使うとプロジェクトの依存に基づいた、かつグローバルからは隔離され独立した環境を定義できます。

でも

便利そうだから使いたいな〜と思っていたものの、具体的にどうすればいいのかが私には全然わかりませんでした。 そもそもNixはまだ手探り状態だし、調べたら nix-shell とか nix develop とか色々出てくるし、いざnix develop してみたら bash しか立ち上がらないし...

devShellを実用するには、せめて普段使っているシェルの環境のままで使えるようにしたいですよね。 依然としてNixのことは全然わかっていませんが、devShellがいい感じに使える方法にたどり着いたのでその方法を記しておこうと思います。

Nixまわりはいろんな情報が散らばりまくっていてしんどいので、「こうしたほうがいいですよ〜」とか、「これ読むといいですよ〜」とかがあればぜひ教えてください。

こうすれば動く

次のようにやってみたらいい感じになりました。以下でそれぞれ説明していきます。

  1. direnv をインストールする
  2. プロジェクトに flake.nix を作り、依存・devShell を定義する
  3. devShell を立ち上げたいプロジェクトでuse flake と記した .envrc を用意する

direnv をインストールする

direnv は、環境ごとに読み込む環境変数などをいい感じにやってくれるツールです。 私はhome-managerでインストールしました:

{
  programs.direnv = {
    enable = true;
    nix-direnv.enable = true;
  };
}

プロジェクトに flake.nix を作り、依存やdevShell を定義する

対象となるプロジェクトにてflake.nixを作り、依存を定義しておきます。 たとえばNode.jsを扱うプロジェクトだと、flake.nixは次のようになります。

flake.nix

{
  description = "...";

  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs/nixos-23.11";
    flake-utils.url = "github:numtide/flake-utils";
  };

  outputs = {nixpkgs, flake-utils, ...}:
    let
      utils = flake-utils;
    in
    utils.lib.eachDefaultSystem(system:
      let 
        pkgs = nixpkgs.legacyPackages.${system};
      in
      {
        formatter = pkgs.nixpkgs-fmt;

        devShell = pkgs.mkShell {
          nativeBuildInputs = with pkgs; [
            nodejs-slim_20
            corepack_20
            yarn
          ];
        };
      }
    );
}

devShell を立ち上げたいプロジェクトでuse flake と記した .envrc を用意する

echo "use flake" > .envrc

すると

この設定ができると、cd でプロジェクトのディレクトリに入るだけでdevShellに定義した依存がactivateされ使えるようになり、シェルもzshのまま使えます。

番外編

devShellを用いた開発を運用するにあたり、次のような設定も行いました。

gitignore

グローバルで.direnv/.envrcをignoreするようにしました。

{
  programs.git = {
    # ...
    ignores = [ ".envrc" ".direnv/" ];
  };
}

VSCode

ファイルエクスプローラ及びファイル検索において、.direnvの内容を表示しないようにしました。

settings.json

{
    "files.exclude": {
        "**/.direnv": true
    },
    "search.exclude": {
        "**/.direnv": true
    }
}

おわりに

Nixは情報がいろんな場所に散らばっていて大変だと思いました。アドバイス等あればぜひ教えてください!!!

React Native で戻るボタンの挙動を改変する

背景

React Native で、スタックナビゲーションを使わずReactの要素として自前のモーダルを利用する機会がありました。

フレームワークの作法から外れて自前でナビゲーションをやるときに問題になるのは、OSが提供する機能との相互運用性です。今回の場合では、モーダルが開いているときにAndroidの戻るボタンが押されたら、前に表示していたスクリーンに戻るのではなく同一のスクリーンのままでモーダルを閉じるような動作を実現したくなります。

この記事では、React Native の BackHandler を用いて「戻る」機能の動作を制御し、モーダル等の開閉状態と繋ぎこんだ動作を実現するコードスニペットを紹介します。

環境

  • React Native 0.73
  • Expo SDK 50
  • Expo Router 3.4

直接関係があるのは React Native のバージョンのみです。

方針

React Native の機能である BackHandler を利用すると、OSで用意されている「戻る」機能の動作を制御することができます。

reactnative.dev

コード例

expo-routerが提供している useFocusEffect を利用しています。 react-navigation を使っている場合でも同じコードが動作するはずです。

// backhandler をインポート
import { BackHandler } from "react-native";

import { useFocusEffect } from 'expo-router';
import { useCallback, useState } from 'react';
// モーダルの開閉を表すステート
const [isModalOpen, setIsModalOpen] = useState(false);
  
useFocusEffect(useCallback(() => {
  const closeModal = () => {
    setIsModalOpen(false);
    return true;
  };

  // モーダルが開いているときはBackHandlerにイベントリスナを追加、モーダルが閉じている場合はnullを返す  
  const handler = isModalOpen
    ? BackHandler.addEventListener('hardwareBackPress', closeModal)
    : null;

  // イベントリスナを追加したときだけクリーンアップ時に削除する
  return () => handler?.remove();

}, [isPreviewModalOpen]));