2012年7月26日木曜日

Make のポータビリティについて考える

Makefile のポータビリティについて考えてみる。

Makefile 中では、 $(MAKE)$(AWK) など、変数で記述することがよく行われる。
これはポータビリティを考える上で当然である。

たとえば、Linux 上で普通 make といえば、GNU make のことを指す。
一方、Free BSDでは、make は GNU拡張されていない普通のmake のことであり、GNU makeのコマンド名は gmake だったりする。
そこで、make というコマンド名を直接 Makefile 中に埋め込むのではなく、 $(MAKE) という変数名で記述するわけだ。

今回、考えたいのは echo コマンドについて。Makefile 中でもよく使われている。
echo コマンドは環境によって、仕様にかなり違いがあり、シェルビルトインのechoもあるので、ポータビリティを考えだすと結構複雑だったりする。

例えば、bash を使っている人は

$ echo "hoge\npiyo"
hoge\npiyo
$ echo -e "hoge\npiyo"
hoge
piyo
$ type echo
echo はシェル組み込み関数です

となる。

一方、tcsh では

$ echo "hoge\npiyo"
hoge
piyo
$ echo -e "hoge\npiyo"
-e hoge
piyo
$ which echo
echo: シェルに入っているコマンドです.

となる。


最近の Ubuntu や Mint に標準で入っている dashの場合、

$ echo "hoge\npiyo"
hoge
piyo
$ echo -e "hoge\npiyo"
-e hoge
piyo
$ type echo
echo is a shell builtin


つまり、bash, tcsh, dash いずれもシェルビルトインのechoを持つが、bashビルトインのechoはデフォルトでは \n といったエスケープシーケンスを解釈してくれず、エスケープシーケンスを解釈させるためには -e オプションをつける必要がある。

一方、tcsh や dash のビルトインのecho はデフォルトでエスケープシーケンスを解釈してくれる代わりに、 -e オプションは解釈してくれない。

さらにシェルビルトインではない /bin/echo の仕様はプラットフォームごとに違うのだが、Linux であれば

$ /bin/echo "hoge\npiyo"
hoge\npiyo
$ /bin/echo -e "hoge\npiyo"
hoge
piyo

となり、bashビルトインのechoと同じような挙動をする。

このようにechoの仕様は結構違う。

そこで、Makefile の中で echo を使った場合、call される echo はどれなのか?
(ここでは GNU make についてのみ考える。)

これについては O'REILLY の「GNU Make」 の5章の冒頭に書かれている。
原著は以下で参照できる。

要約するとこういうことだ。
・make はコマンドを1行ごとに取り出し、サブシェルに渡して実行する。
・ただし、ワイルドカードやリダイレクションなどシェルの特殊文字が使われていない場合、高速化のためにサブシェルに渡さず、makeが直接コマンドを実行する。
・呼び出すサブシェルはデフォルトで /bin/sh であるが、make の変数 SHELL により変更できる。
・ただし、この変数は環境変数から引き継ぐものではない。make は環境変数を makeの変数として取り込むが、 SHELL だけは例外である。


本当にそうなっているか、以下の Makefile で実験してみる。


all1:
@echo --version
@type echo

all2:
@echo --version | cat
@type echo | cat

all3: SHELL=/bin/bash
all3:
@echo --version
@type echo



以下は Fedora 上で実行した結果である。

$ make all1
echo (GNU coreutils) 8.15
Copyright (C) 2012 Free Software Foundation, Inc.
ライセンス GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>.
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

作者 Brian Fox および Chet Ramey。
make: type: コマンドが見つかりませんでした
make: *** [all1] エラー 127
$ make all2
--version
echo はシェル組み込み関数です
$ make all3
--version
echo はシェル組み込み関数です



all1 は make が直接コマンドを実行しており、 /bin/echo が実行されている。
(そのため、 --version オプションを解釈している。)

all2  はパイプを使っている。ワイルドカードやリダイレクト、パイプなどを使っているとサブシェル /bin/sh の中で実行する。
Fedoraでは /bin/sh は bashへのシンボリックリンクになっているので、結局は bashビルトインのecho を実行している。(そして --version オプションを解釈しない

all3 は make 変数 SHELL の値を /bin/bash に書き換えているので、bash ビルトインのecho を実行している。

前述のように、make 変数の SHELL は環境変数の SHELL とは無関係だ。
SHELL だけは環境変数から引き継が仕様になっている理由は、ユーザーの使っているシェルがmakefileの動作に影響しないためだ。
例えば、tcsh をデフォルトにしているユーザーは 環境変数 SHELL/bin/tcsh になっているはずだが、だからといって、 makefile のルールを tcsh で実行するのはナンセンスだ。スクリプトを C shell系で記述することはほとんどないわけだから。

all1all2 の実行結果の違いを見てもわかるように、ワイルドカード、リダイレクト、パイプなどのシェルの特殊文字を使っているかどうかによって、サブシェルを呼ぶか、make が直接実行するかが変わる点は注目に値する。

make がサブシェルを呼ぶ場合、makefile 内で明示的に変数 SHELL を書きかえない限りは、 /bin/sh を呼ぶ。
Redhat 系ならば /bin/sh はbash へのシンボリックリンクだが、Ubuntu や Mint は dash へのシンボリックリンクになっているので、前述のように echo の動作が変わってくることになり、環境によっては期待通りに動いてくれないかもしれない。
環境によっては /bin/sh がシンボリックリンクではなく、本物の Bourne Shell なこともあるだろう。

対策として、 ECHO=/bin/echo のように変数を用意し、 $(ECHO) を使用することで、常に /bin/echo を実行するようにするということが考えられる。

サポートするビルド環境を、Linux などの単一のプラットフォームに限定するならば、これでもよい。
だが、 /bin/echo の仕様はプラットフォームごとに結構違うので、別のプラットフォームに移植するときに問題になり、ポータビリティには欠けると思う。

例えば、Linux の /bin/echo-e オプションを解釈するが、FreeBSDの /bin/echo は解釈しない。
また、echo の改行を抑制するためには、Linux や FreeBSD では -n オプションを使用するが、Solaris や HP-UX は
echo "hoge\c"
のように \c で改行を抑制するようだ。

そういうわけで、多様なプラットフォームへの対応しないといけない場合、
Makefile に
SHELL=/bin/bash
と書いておき、常にbashビルトインのechoを使わせるようにするのがいいのではないかと思った。
(本当にこれで大丈夫なのかは確認していないのですが。。)
ただし、その場合、make が直接コマンドを実行するという最適化は行われなくなる。

まあ、一番良いのは、あまりマニアックなオプションは使わないにこしたことはないです。

0 件のコメント:

コメントを投稿