Makefile で設定した PATH が効かないとき

f:id:takasfz:20220405215531p:plain

スクランナー的な用途で make を使っていて、パスが通ってるんだけど通ってない…?みたいな現象に出会ったときのメモです。

PATH := $(HOME)/.pub-cache/bin:$(PATH)

.PHONY: path
path:
    which fvm
    fvm --version

↑ のような Makefile があったとき、このタスクを実行すると

$ make path
which fvm
/Users/takasfz/.pub-cache/bin/fvm
fvm --version
make: fvm: No such file or directory
make: *** [path] Error 1

↑ のように「 which だと見つかるが、コマンドを実行しようとすると見つからない」状態でした。

何が起きていたか

make の仕様には、「通常はレシピの各行に対して新しいサブシェルを起動して実行するが、結果に影響しないショートカットをする場合もある」とあります。

https://www.gnu.org/software/make/manual/make.html#Execution

5.3 Recipe Execution

When it is time to execute recipes to update a target, they are executed by invoking a new sub-shell for each line of the recipe, unless the .ONESHELL special target is in effect (see Using One Shell) (In practice, make may take shortcuts that do not affect the results.)

「結果に影響しないショートカット」とは具体的には、ただコマンドを実行するだけの場合、シェルを起動せずに直接コマンドを実行するようです。この挙動のことを fast path っていう言い方をするっぽい。

https://stackoverflow.com/a/64202705

... just have a simple command invocation with no shell features like multiple commands, special quoting, globbing, etc. then make uses the "fast path" to invoke your command.

That is, if make can determine that the shell would do nothing special with your command, other than run it, make will skip invoking the shell and instead run your command directly.

「ただコマンドを実行するだけの場合」の判定は make のソースコードに書いてあって、

  • sh_chars に定義されている文字のいずれかが行に含まれている
  • sh_cmds に定義されているコマンドのいずれかが行の先頭に含まれている

のどちらかの場合はシェルを起動、それ以外は直接コマンドを実行します。

http://git.savannah.gnu.org/cgit/make.git/tree/src/job.c#n2656

Figure out the argument list necessary to run LINE as a command. Try to avoid using a shell. This routine handles only ' quoting, and " quoting when no backslash, $ or ' characters are seen in the quotes. Starting quotes may be escaped with a backslash. If any of the characters in sh_chars is seen, or any of the builtin commands listed in sh_cmds is the first word of a line, the shell is used.

で、 Apple のシステムに入ってる make にはバグがあるようで、 fast path で直接コマンドを実行するときに Makefile 上で設定した PATH が反映されません。

https://stackoverflow.com/a/21709821

It appears that for some reason the version of GNU make shipped by Apple is not working properly in that it's not setting the environment correctly for commands which are run directly by GNU make, via the "fast path".

その結果、 which fvm は fvm の実行ファイルのパスを正しく出力しているのですが、単に fvm コマンドを実行しようとすると見つからないと言われていました。

回避策

この問題を回避するためには fast path が使われないようにすればいいので、 PATH を効かせたい行に any of the characters in sh_chars である ; をつけるようにします。

PATH := $(HOME)/.pub-cache/bin:$(PATH)

.PHONY: path
path:
    which fvm
    fvm --version;
$ make path
which fvm
/Users/takasfz/.pub-cache/bin/fvm
fvm --version;
2.2.6

何をしたかったのか

Flutter でチーム開発をしていて必要なツールが増えてきたときに、あれもこれもインストールしてパスを通して、 Ruby はシステムデフォルトだと NG で CocoaPods は gem install じゃないとダメで、っていう手順を README に書くのがしんどくなってきた + 新しくチームに加わる人のセットアップ完了までのハードルが高くなってきたのを解決するために、 Homebrew だけインストールすればあとは make setup で完結するようにしたかったのでした。

最終的には、こんな感じの Makefile になりました。

PATH := $(HOME)/.pub-cache/bin:$(HOME)/.rbenv/shims:$(PATH)

.PHONY: build
build:
    :; melos run pub:get
    :; melos run gen

########################################
# Setup
########################################

.PHONY: install-dart
install-dart:
    # Dart
    brew tap dart-lang/dart
    brew install dart

.PHONY: install-ruby
install-ruby:
    # Ruby
    brew install rbenv
    rbenv install -s

.PHONY: install-dev-tools
install-dev-tools:
    # Flutter
    dart pub global activate fvm
    :; fvm install

    # Melos
    dart pub global activate melos

    # CocoaPods
    :; gem install cocoapods
    :; gem install cocoapods-keys

.PHONY: prepare-build
prepare-build:
    # dependencies
    :; melos run pub:get
    :; melos run precache:ios

.PHONY: setup
setup: install-dart install-ruby install-dev-tools prepare-build build
    @printf '\n\033[33m%s\n%s\033[0m\n\n' \
        'Add $$HOME/.pub-cache/bin to your $$PATH environment.' \
        'Add '"'"'eval "$$(rbenv init -)"'"'"' to your profile.'

fvm --version; みたいに行末に ; をつけてもいいんですが、あえて違和感ある書き方をするほうが「意図的にやってる」のに気付きやすいかと思って、何もしないコマンド : と組み合わせた :; を行の先頭に書くようにしました。( https://stackoverflow.com/a/21709821 の補足コメントでもこの方法が紹介されています。)


※ ちなみに、先の StackOverflow に書いてあった通り、 mac のシステム make じゃなくて Homebrew でインストールした make ( installed as "gmake". )を使うと ; なしでも大丈夫でした。

$ make -v
GNU Make 3.81
Copyright (C) 2006  Free Software Foundation, Inc.
This is free software; see the source for copying conditions.
There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE.

This program built for i386-apple-darwin11.3.0
$ gmake -v
GNU Make 4.3
Built for arm-apple-darwin20.2.0
Copyright (C) 1988-2020 Free Software Foundation, Inc.
License 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.