Article:

wslではexeがexecutableとしてマークされており、通常バイナリと同様の手順で起動することができます。
当然linuxのsyscallが使えるわけではないので、起動すると言ってもホスト側で起動してくれるだけですが、これは絶妙に便利な機能ですね。 参考

たとえば、clip.exeがpbcopyと同じノリで使えるのもこれのおかげです。

こういった特殊な環境下なので、wsl上でwindows向けのバイナリを作ろうというケースにおいて、クロスコンパイルのような複雑なことを行う必要はなく、 ただwsl上からwindows上のc++コンパイラ・リンカであることのcl.exe link.exeをそのまま起動してあげれば良いだけとなります。

とは言えc++のプロジェクトを毎回コンパイラのコマンドを直接叩いてビルドするのは苦痛ですから、当然万能makefileができるまでの記事で書いたようなmakefileが欲しくなるわけです。

大部分はそのまま使えるのですが、細かいところに変更が必要だったので最終形を貼りつつ、要点については個別に解説します。

makefile
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
PROG := hello.exe
SRCS := $(wildcard *.cpp)
OBJS := $(SRCS:%.cpp=%.o)
DEPS := $(SRCS:%.cpp=%.d)
DEPJ := $(SRCS:%.cpp=%.d.json)

CC := /mnt/c/Program\ Files/Microsoft\ Visual\ Studio/2022/Community/VC/Tools/MSVC/14.33.31629/bin/Hostx64/x64/cl.exe
CL := /mnt/c/Program\ Files/Microsoft\ Visual\ Studio/2022/Community/VC/Tools/MSVC/14.33.31629/bin/Hostx64/x64/link.exe
CCFLAGS := /std:c++20 /MD
CLFLAGS := /NODEFAULTLIB:library
INCLUDEPATH := -I 'C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.33.31629\include'\
               -I 'C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.33.31629\ATLMFC\include'\
               -I 'C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Auxiliary\VS\include'\
               -I 'C:\Program Files (x86)\Windows Kits\10\include\10.0.19041.0\ucrt'\
               -I 'C:\Program Files (x86)\Windows Kits\10\\include\10.0.19041.0\\um'\
               -I 'C:\Program Files (x86)\Windows Kits\10\\include\10.0.19041.0\\shared'\
               -I 'C:\Program Files (x86)\Windows Kits\10\\include\10.0.19041.0\\winrt'\
               -I 'C:\Program Files (x86)\Windows Kits\10\\include\10.0.19041.0\\cppwinrt'

LIBPATH := -LIBPATH:'C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.33.31629\ATLMFC\lib\x64'\
           -LIBPATH:'C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.33.31629\lib\x64'\
           -LIBPATH:'C:\Program Files (x86)\Windows Kits\10\lib\10.0.19041.0\ucrt\x64'\
           -LIBPATH:'C:\Program Files (x86)\Windows Kits\10\\lib\10.0.19041.0\\um\x64'

LIBS := 

all: $(PROG) $(DEPS)

$(PROG): $(OBJS)
	$(CL) $(CLFLAGS) /out:$@ $^ $(LIBPATH) $(LIBS)

%.o %.d.json: %.cpp
	$(CC) $(CCFLAGS) $(INCLUDEPATH) -c $$(wslpath -w $<) -Fo$(<:%.cpp=%.o) -sourceDependencies $(<:%.cpp=%.d.json)

%.d: %.d.json
	python3 json2depend.py $< $(<:%.d.json=%.o) $(<:%.d.json=%.cpp) $(<:%.d.json=%.d)

include $(shell ls ${DEPS} 2>/dev/null)

clean:
	$(RM) $(PROG) $(OBJS) $(DEPS)

.PHONY: all clean

注意:
windowsはパスの大文字小文字を区別しないので、cl.exeが吐き出してくれる依存関係のファイルではパスがすべて小文字になっています。
これをmakeにそのまま適応すると、ファイルが見つけられなくて死にます。なので、wslルート以下のプロジェクトまでのパスは、すべて小文字になるようにしておく必要があります…。

またこのmakefileを使うには、後述するpythonスクリプトが必要です

コマンドやライブラリのパスの調べ方

vscodeをインストールした際に一緒に入ってくるDeveloper Command Prompt for VS 2022がすべてを知っています。

Developer Command Promptを起動し、以下のコマンドで調べることができます。

cl.exe, link.exe
1
2
> where.exe cl.exe
> where.exe link.exe
includepathとlibpath
1
2
echo %include%
echo %lib%

依存関係ファイルの変換

gccでは-MP -MMDオプションでmake互換の依存関係ファイルを出力してくれますが、cl.exeはそんなことはしてくれません。 その代わり、-sourceDependenciesオプションを使うとjson形式で出力してくれるようなので、これをmake互換のものに変換してくれるスクリプトを別途用意しました。

pythonライブラリ jinja2が必要です。

json2depend.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import re
import sys
import json
from jinja2 import Template

jsonfile = sys.argv[1]
objname = sys.argv[2]
srcname = sys.argv[3]
dstname = sys.argv[4]

def convert_wslpath(path):
    m = re.match(r'\\\\wsl\$\\[^\\]*\\(.*)', path)
    if m:
        return '/' + m[1].replace('\\', '/')

tmpl = (
    '{{ objname }}: {{ srcname }} {% for elem in includes %}{{ elem }} {% endfor %}\n'
    '{% for elem in includes %}'
    '{{ elem }}:\n'
    '{% endfor %}'
)

with open(jsonfile, 'r') as f:
    srcDepend = json.load(f)

includes = list(filter(None, map(convert_wslpath, srcDepend['Data']['Includes'])))

rendered = Template(tmpl).render({
    'includes': includes,
    'objname': objname,
    'srcname': srcname,
    'dstname': dstname
    })

with open(dstname, 'w') as f:
    f.write(rendered)

wsl内のパス意外は無視する実装にしちゃっているので(大抵の使い方だとこれで良いと思ってはいますが)そこだけ注意してください。

へんなinclude

いつもだったら -include $(DEPS) とするところなのですが、今回DEPSを生成するルールをちゃんと書いているので、 makeが気を利かせてちゃんとincludeできるように毎回作ろうとしてくれます(参考: GNU Make は include 先が見つからなくてもルールさえあれば生成して include してくれる)。

これが今回のケースでどう不都合かというと、すでにcleanされてたり、make途中で失敗して生成ファイルが不十分だった時にmake cleanすると、ご丁寧に再ビルドを行ってからcleanしてくれます。

これは気持ち悪いので、手動でDEPSのうち存在しているものだけを与えてあげるというのが今回記載したルールになります。