🍺

環境変数などの設定が必要なHomebrew packageをEnvironment Modulesで管理する

2023/09/25に公開

はじめに

macOSでHomebrewを使っている方はpackageを追加した際に以下のような表示を見たことがあるのではないでしょうか?

terminal
llvm is keg-only, which means it was not symlinked into /opt/homebrew,
because macOS already provides this software and installing another version in
parallel can cause all kinds of trouble.

書いてある通りですが, macOSや他のformulaが提供しているsoftwareと競合するpackageをHomebrewでinstallしようとするとsymbolic linkは作成されません.

そのため, keg-onlyのpackageを使用するには手動で環境変数などの設定を行う必要があります
[1].
どうやって設定を行えば良いかというと基本的にpackageをinstallした際に表示されるmessageに書いてあります.
例えば, llvmの場合は以下のようになっています
llvmはC(++) compilerのLLVM Clangのことです.
正確にはllvmを入れるとLLVM Clang以外も色々入る気がしますが, ややこしいので以下ではLLVM Clangという呼び方をします.

terminal
To use the bundled libc++ please add the following LDFLAGS:
  LDFLAGS="-L/opt/homebrew/opt/llvm/lib/c++ -Wl,-rpath,/opt/homebrew/opt/llvm/lib/c++"

llvm is keg-only, ... # 上記のmessageのため省略

If you need to have llvm first in your PATH, run:
  echo 'export PATH="/opt/homebrew/opt/llvm/bin:$PATH"' >> ~/.zshrc

For compilers to find llvm you may need to set:
  export LDFLAGS="-L/opt/homebrew/opt/llvm/lib"
  export CPPFLAGS="-I/opt/homebrew/opt/llvm/include"

まとめると環境変数PATH, LDFLAGS, CPPFLAGSを設定せよということです.

基本的に書いてあるcommandをterminalで実行すれば動きますが, 上記commandには2つ問題点があります.

1つ目はPATH変数の設定です.
上記のmessageの該当部分は

terminal
If you need to have llvm first in your PATH, run:
  echo 'export PATH="/opt/homebrew/opt/llvm/bin:$PATH"' >> ~/.zshrc

で, .zshrcPATH変数を設定するように書かれています.
このcommandを実行するとterminalの起動時に自動的にLLVM Clangが使用可能になります.
この方法の問題は, macOSに入っているApple Clang[2]が見つからなくなることです.
自分でApple Clangを使いたい場合や, 他のpackageなどがApple Clangを前提としている場合は不具合が発生する可能性があります.

2つ目は環境変数などを毎回設定するのが面倒ということです.
上記のcommandを覚えたり, どこかに記録しておかないといけません.
また, 上記のmessageはinstallした際にしか表示されないので1度忘れると再確認するのも面倒です.brew info llvmで再表示可能でした.

これらの問題を解決するためにEnvironment Modulesを用いて環境変数などの設定を管理します.
Environment Modulesは環境変数などを管理するためのpackageで, 共同利用スパコンでよく使われています.
Environment Modulesを導入することで

terminal
module load llvm

というcommandを実行するだけでPATH, CPPFLAGS, LDFLAGSが設定されるようになります.

また,

terminal
module unload llvm

で元に戻すことができ, Apple Clangとの使い分けも容易になります.

この後は

  • Environment Modulesでpackageを管理するためのmodulefileの作成
  • Environment Modules packageの導入

という流れになります.
今回はllvm (LLVM Clang)を例にして説明します.
内容としては通常のEnvironment Modulesの導入と大差はありませんが, Homebrew+Environment Modulesの設定の具体例として参考にしていただければと思います.
keg-only packageの管理方法として他に良い方法があれば教えていただきたいです.

modulefileの作成

まずはEnvironment Modulesで各packageを管理するためのmodulefileを作成します.

modulefileを置いておくdirectoryを決めてください.
ここではMODULEFILE_PATHとします.
MODULEFILE_PATHmodulefilesというdirectoryを作成して, llvmという設定fileを以下のように作成します.

/MODULEFILE_PATH/modulefiles/llvm
#%Module

conflict llvm

set root "$::env(HOMEBREW_PREFIX)/opt/llvm"

# set PATH
prepend-path PATH "$root/bin"

# set CPPFLAGS
if { [info exists ::env(CPPFLAGS)] } {
    setenv CPPFLAGS "-I$root/include $::env(CPPFLAGS)"
} else {
    setenv CPPFLAGS "-I$root/include"
}

# set LDFLAGS
if { [info exists ::env(LDFLAGS)] } {
    #setenv LDFLAGS "-L$root/lib $::env(LDFLAGS)"
    # To use the bundled libc++
    setenv LDFLAGS "-L$root/lib -L$root/lib/c++ -Wl,-rpath,$root/lib/c++ $::env(LDFLAGS)"
} else {
    #setenv LDFLAGS "-L$root/lib"
    # To use the bundled libc++
    setenv LDFLAGS "-L$root/lib -L$root/lib/c++ -Wl,-rpath,$root/lib/c++"
}

modulefileはTcl言語で記述されます.
詳細については述べませんが簡単に記載内容について説明します.

  • modulefileのfile名は任意です. ここではllvmとしていますがversion名など他の名前でも問題ありません. 実際に使用する際にはmodule load llvmというようにmodulefileのfile名を指定するため, 命名規則を明確にしておいた方が良いでしょう.
  • #%Modulemodulefileの先頭に必ず書く必要があります.
  • conflict llvmは同時にloadできないmoduleを指定するための設定です
    [3].
  • set rootrootというmodulefile内で用いる変数を設定しています.HOMEBREW_PREFIX(AArch64版Homebrewの場合のdefaultは/opt/homebrew)はHomebrewによってinstallされたpackageのroot directoryの場所を指定する環境変数です.これを$::env(HOMEBREW_PREFIX)として取得しrootに代入しています.
  • PATHprepend-pathで設定します. prepend-pathは現在のPATH変数の先頭に追加します.
  • append-pathで現在のPATHの末尾に追加することもできます.
  • 環境変数はsetenvで設定します.
  • setenvprepend-pathappend-pathと異なり現在の環境変数を上書きします.
    そのため, 環境変数が既に設定されているか否かを判定し, 動作を変えるようにしています.
  • LDFLAGSの設定が少し複雑なのは, LLVMの標準librarylibc++を使う設定をしているためです. Apple Clangにbundleされたlibc++を使用して問題ない場合はcomment outしている部分の設定を代わりに使ってください.

他のpackageについても同様にmodulefileを作成することでEnvironment Modulesで管理できるようになります.

Environment Modulesの導入

Environment Modulesを導入します.
Homebrewで入ります:

terminal
brew install modules

次に, module commandを使用するために.zshrcに以下を追記します[4].
MODULEFILE_PATHmodulefilesを置くdirectoryのpathを記載してください.
1行目に関してはEnvironment Modulesをinstallした際に出てくるmessageに書いてあるcommandを参照した方が確実です.

.zshrc
source $HOMEBREW_PREFIX/opt/modules/init/zsh
module use /MODULEFILE_PATH/modulefiles

追加したらterminalを再起動するかsource ~/.zshrcを実行して設定を反映させます.

module commandが使えるか確認します.

terminal
module --version
Modules Release 5.3.1 (2023-06-27) #出力結果

これでEnvironment Modulesの導入は完了です.
先に記載した通り,

terminal
module load llvm

でLLVM Clangが使えるようになります.
module commandの詳しい使い方は公式documentなどを参照してください.

参考文献

https://modules.readthedocs.io/en/latest/modulefile.html
https://x.momo86.net/article/36#config
https://qiita.com/Ag_smith/items/f268ad27165a60aecd35

脚注
  1. なお, keg-onlyというのは直訳すると「樽だけ」となります.
    Homebrew(自家酒造)ではCellar(地下貯蔵室) directoryでpackageが管理されているため, keg-only(樽だけ)というのはCellar内に留めて外に出さないということなのだと思います.
    知らんがな. ↩︎

  2. 正確にはXcodeかCommand Line Tools for Xcodeを入れる必要がありますが, Homebrewを入れる際に必要なので入っているはずです. ↩︎

  3. これは, 例えばLLVM Clangの異なる複数のversionを管理する場合に, 異なるversionのLLVM Clangを同時にloadすることができないようにするというような使い方をします.
    ここではmodulefileのfile名と同じ名前をconflictで指定しているので少し紛らわしいですが, conflictで指定する名前はmodulefileのfile名とは関係なく, conflictで指定した名前で衝突判定が行われます. ↩︎

  4. bashなど, 他の環境を使っている方は適宜読み替えてください. source commandの内容も変わるはずなので, Environment Modulesをinstallした際に表示されるmessageを確認してください ↩︎

GitHubで編集を提案

Discussion