Windows でも複階層化したソースコードを Makefile でビルドしたい

#1. 複階層化したソースコードを Makefile でビルドするには

　ソースコードが複数のディレクトリ階層にわたって存在する場合に使える Makefile の書き方として，[ソースが複階層化された場合のMakefile（第五回）](http://gmaj7sus4.hatenablog.com/entry/2013/12/18/165224) が非常に参考になる。 普段はこれを参考に作成した Makefile を使っていたが，Windows 環境下で使おうとした場合，シェルコマンドの役割が違っていたりするためにうまく動かない。 ちょっとググってみた限りでは，ぴったりと合う解決策は見当たらなかった。

#2. Windows での問題点
　上記の Makefile を Windows 環境下で使うには，いくつかの問題がある。 Windows 上でも使えるように，環境スイッチ変数 `ENV` を用意して，Windows でも動作するように対策を施したい。

##2.1. mkdir の動作の違い
　Unix シェルでは `mkdir -p a/b/c` のように -p オプションを指定することで，ディレクトリ a や a/b が存在しない場合でも a/b/c までディレクトリ構造を掘り進んでくれる。 しかしながら，Windows で同様のコマンドを実行すると，-p というディレクトリと a/b/c というディレクトリがそれぞれ作られてしまう。

　そもそも Windows (コマンドプロンプト) 上では，オプションの prefix は / (スラッシュ) で表わされる。 このため，-p はオプションではなく "-p" という名前のディレクトリが指定されたものだと思われてしまう。 Windows の mkdir はオプションを指定しなくても新しいディレクトリのパスを a/b/c のように指定するだけでディレクトリを掘り進んでくれるため，-p の指定は冗長なのである。

```mk:Makefile(抜粋)
ENV = Windows #Unix 環境の場合はコメントアウト

#(中略)

MKDIR = mkdir -p
ifeq ($(ENV),Windows)
  MKDIR = mkdir
endif
```

##2.2. find の機能の違い
　Unix シェルの `find` コマンドと windows 上の `find` コマンドとでは，その機能がまったく違う。 Unix シェル下では `find` がファイル検索に使われるのに対し，Windows 環境 (コマンドプロンプト) 下では `find` はファイル内のテキスト検索に使われる。 オリジナルの Makefile では `find` でファイルを検索して，その結果からソースファイルのリストを作成しているが，windows ではそのような使い方はできない。

　Windows 上では，ファイルの一覧を取得するのに `dir` コマンドが使われる。 `dir /b /s src` とすることで，src ディレクトリ配下のファイルリストを取得することができる。 と，ここまでは簡単なのだが，`dir` コマンドで取得されるファイルリストは絶対パス表記になのが悩みどころ。 オブジェクトファイルのリストを作成する際に，ソースファイルリストの拡張子とディレクトリ名を置換が行われるが，ソースファイルリストが絶対パス表記だと，置換対象のディレクトリ名がパス文字列の中間に現れてしまうために都合が悪い。

　結局，ソースファイルのリストを作成する際に，カレントディレクトリを表す部分を置換によって削除するという力技で~~強引に~~ごまかすことにした。

```mf:Makefile(抜粋)
ENV = Windows #Unix 環境の場合はコメントアウト
#(中略)
ifeq ($(ENV),Windows)
  CPPS = $(shell dir /s /b)
  CPPS := $(patsubst $(shell cd)\\%,%,$(CPPS))
  CPPS := $(filter %.cpp,(CPPS))
else
  CPPS = $(shell find  * -name *.cpp)
endif
```
##2.3. ディレクトリ区切り文字の違い
 Unix のディレクトリ区切り文字が `/` であるのに対して，Windows では `\` である。 Makefile において，`\` はエスケープ文字としても使用されるため，いろいろと予期しない動作を引き起こす。

　たとえば，上記の対策を実施した Makefile を Windows 上で実行すると， `@[ -d "$(BINDIRS)" ] || $(MKDIR) $(BINDIRS)` の行で 「コマンドの構文が誤っている」 と怒られてしまう。 `$(BINDIRS)` に含まれるディレクトリのパスの末尾が `\` で終わっているため，エスケープが発生しているものと思われる。

　上記の問題は，パス文字列中のディレクトリ区切り文字 `\` を `/` で置換することで解消する。 しかしながら，この置換は直後に別の問題を引き起こす。 ディレクトリ区切りを置換すると `$(MKDIR)` の引数に `/` が含まれることになるため，mkdir がこれをオプションであると勘違いし，またしても「コマンドの構文が誤っています。」 と言われてしまうのである。 したがって，mkdir する個所だけは，ディレクトリ区切り文字を `\` に戻してあげる必要がある。

　結局，ここでも強引な置換作戦によって対策することとした。

```mf:Makefile(抜粋)
ENV = Windows #Unix 環境の場合はコメントアウト

#(中略)

ifeq ($(ENV),Windows)
  CPPS = $(shell dir /s /b)
  CPPS := $(patsubst $(shell cd)\\%,%,$(CPPS))
# この Makefile 内のパス情報は $(CPPS) から作られているので
# この時点でディレクトリ区切りを "\" に置換しておく。
# というわけで ↓ 一行追加
  CPPS := $(subst \,/,$(CPPS))
  CPPS := $(filter %.cpp,(CPPS))
else
  CPPS = $(shell find  * -name *.cpp)
endif

#(中略)

BINDIRS = $(addprefix $(OBJDIR)/, $(DIRS))
# MKDIR する部分をマクロ化
MKOBJDIR = $(MKDIR) $(OBJDIR)
MKBINDIRS = $(MKDIR) $(BINDIRS)
ifeq ($(ENV),Windows)
# ↓ Windows 下の場合，MKDIR の引数になる部分だけ区切り文字を \ に置換
  MKOBJDIR = $(MKDIR) $(subst /,\,$(OBJDIR))
  MKBINDIRS = $(MKDIR) $(subst /,\,$(BINDIRS))
endif

#(中略)

default:
	# ↓ ディレクトリ作成マクロを使用するように変更
	@[ -d  $($OBJDIR)    ] || $(MKOBJDIR)
	@[ -d  "$($BINDIRS)" ] || $(MKBINDIRS)
```

#3. まとめ
　[複階層化したソースコードを Windows 上でビルドするための Makefile](http://gmaj7sus4.hatenablog.com/entry/2013/12/18/165224) は，以下の処置を実施することで Windows 上でも利用することができた。

* `mkdir -p` とするところを Windows では `mkdir` とする。
* `find` の代わりに `dir` を使う。 ただし，絶対パスから相対パスへの書き換えが必要。
* パス文字列におけるディレクトリ区切り文字は `/` に置換する。 ただしパス文字列を `mkdir` の引数に使うときに限り，ディレクトリ区切りを `\` に再置換する。

　間違いや拙い点などありましたら，ご指摘いただけると幸いです。