SConsをちょっと便利なMakeとして使ってみる

makeでちょっと複雑なことをやろうとすると結構しんどくなってきたので、SConsを試してみることにした。

まだ全然使いこなせてないが、とりあえずmakeでできることをSConsではどのようにやるのかのメモ。

単純な例

次のようなMakefileを考えてみる。

all: piyo.txt

hoge.txt:
	echo "This is $@" > $@

fuga.txt:
	echo "This is $@" > $@

piyo.txt: hoge.txt fuga.txt
	cat $+ > $@

SConsではSConstructというファイルが、makeにおけるMakefileに相当する。SConstructはPythonスクリプトである。

上記のMakefileと同様の動きをするSConstructは以下のように書ける。

Command("hoge.txt", None, 'echo "This is $TARGET" > $TARGET')
Command("fuga.txt", None, 'echo "This is $TARGET" > $TARGET')
Command("piyo.txt", ["hoge.txt", "fuga.txt"], 'cat $SOURCES > $TARGET')

Commandに引数として、ターゲット、ソース、アクションを渡す。Makefileでターゲット、ソース、コマンドを書く順と同じなので覚えやすいと思う。
複数のファイルを指定したい時はリストを渡せば良い。

Makefileではソースやターゲットを示す変数として、$+、$<、$@などを用いるが、SConsでは$SOURCES、$SOURCE、$TARGETなどを用いる。どのようなものが使えるかはmanのVariable Substitutionセクションに書いてある。

コマンドライン

makeでmakeコマンドでビルドを実行するのと同様に、SConsでもSConstructのあるディレクトリでsconsというコマンドを叩けばビルドが実行される。

sconsのオプションはmakeと同じものが多く覚えるのは比較的楽だと思われる(-jや-nなど)。

clean

SConsではscons -cを実行すると、ビルドしたターゲットを削除してくれる。clean用のコマンドを書く必要はない。

debug

makeとsconsではどのファイルをビルドするかの決定基準が違っているので、下記のようにそのファイルをビルドする理由を表示させると理解が深まるかもしれない。

scons --debug=explain
依存関係の表示

treeオプションを使うと、依存関係をツリー状に表示できる。

scons --tree=all

ファイル名の操作

makeでは関数を使って、ファイル名やパスを扱うことができる。

all: piyo.txt

TXTDIR := txt

$(TXTDIR):
	mkdir -p $@

$(TXTDIR)/hoge.txt:| $(TXTDIR)
	echo abspath: $(abspath $@) > $@
	echo dir: $(dir $@) >> $@
	echo notdir: $(notdir $@) >> $@
	echo basename: $(basename $@) >> $@
	echo suffix: $(suffix $@) >> $@

FUGA_TXT_NAME := fuga.txt
FUGA_TXT := $(TXTDIR)/$(FUGA_TXT_NAME)
FUGA_TXT_ABSPATH := $(abspath $(FUGA_TXT))
FUGA_TXT_DIR := $(dir $(FUGA_TXT))
FUGA_TXT_NOTDIR := $(notdir $(FUGA_TXT))
FUGA_TXT_BASENAME := $(basename $(FUGA_TXT))
FUGA_TXT_SUFFIX := $(suffix $(FUGA_TXT))

$(FUGA_TXT):| $(TXTDIR)
	echo abspath: $(FUGA_TXT_ABSPATH) > $@
	echo dir: $(FUGA_TXT_DIR) >> $@
	echo notdir: $(FUGA_TXT_NOTDIR) >> $@
	echo basename: $(FUGA_TXT_BASENAME) >> $@
	echo suffix: $(FUGA_TXT_SUFFIX) >> $@

piyo.txt: $(TXTDIR)/hoge.txt $(FUGA_TXT)
	cat $+ > $@

上記のMakefileと同様の動作をするSConstructは次のように書ける。

os.pathモジュールを使うこともできるが、${TARGET.abspath}のような書き方もできる。この書き方についての説明もmanのVariable Substitutionにある。

import os.path

txtdir = "txt"

Command(os.path.join(txtdir, "hoge.txt"), None,
        [
            "echo abspath: ${TARGET.abspath} > $TARGET",
            "echo dir: ${TARGET.dir} >> $TARGET",
            "echo notdir: ${TARGET.file} >> $TARGET",
            "echo basename: ${TARGET.base} >> $TARGET",
            "echo suffix: ${TARGET.suffix} >> $TARGET"
        ])

fuga_txt_name = "fuga.txt"
fuga_txt = os.path.join(txtdir, fuga_txt_name)
fuga_txt_abspath = os.path.abspath(fuga_txt)
fuga_txt_dir = os.path.dirname(fuga_txt)
fuga_txt_notdir = os.path.basename(fuga_txt)
fuga_txt_basename = os.path.splitext(fuga_txt)[0]
fuga_txt_suffix = os.path.splitext(fuga_txt)[1]


Command(fuga_txt, None,
        """
            echo abspath: %(fuga_txt_abspath)s > $TARGET
            echo dir: %(fuga_txt_dir)s >> $TARGET
            echo notdir: %(fuga_txt_notdir)s >> $TARGET
            echo basename: %(fuga_txt_basename)s >> $TARGET
            echo suffix: %(fuga_txt_suffix)s >> $TARGET
        """ % locals()
       )

Command("piyo.txt", [os.path.join(txtdir, "hoge.txt"), fuga_txt],
        "cat $SOURCES > $TARGET")
ディレクトリの自動作成

上記の例ではhoge.txtとfuga.txtをtxtというディレクトリの中に作っている。makeの場合自分でtxtを作成する必要があるが、SConsではターゲットの置かれるディレクトリが自動で作成されるためディレクトリを作成するコマンドを書いたり、依存関係にディレクトリを含めたりする必要はない。

関数とか

makeでは関数をうまく使うと記述が簡単になることがある。

TXT_FILES := $(wildcard *.txt)
TXT_FILES_SHELL := $(shell echo *.txt)
PATSUBST := $(patsubst %.txt,%.TXT,$(TXT_FILES))
SUBST := $(subst World,Sekai,Hello World!)


all:
	echo TXT_FILES: $(TXT_FILES)
	echo TXT_FILES_SHELL: $(TXT_FILES_SHELL)
	echo PATSUBST: $(PATSUBST)
	echo SUBST: $(SUBST)

SConsでも同様のことはできるが、makeの関数のようにひとつのやり方があるわけでなく、Pythonの処理を組み合わせて実現している場合があるので、Pythonについて知らないとわかりにくいかもしれない。makeにある関数をSConsで実現するには、User's Guideの最後のHandling Common Tasksを参考にすると良い。

アクションの中に${}で囲まれたPythonのコードを書くと、実行結果に置き換えられる。

import os, re

env = DefaultEnvironment()

txt_files = [str(f) for f in Glob("*.txt")]
txt_files_shell = os.popen("echo *.txt").read()
patsubst = [os.path.splitext(f)[0] + ".TXT" for f in txt_files]
subst = re.sub("World", "Sekai", "Hello World!")

env["patsubst"] = patsubst

all = Command(".", None,
        """
            echo TXT_FILES: ${" ".join(%(txt_files)s)}
            echo TXT_FILES_SHELL: %(txt_files_shell)s
            echo PATSUBST: ${" ".join(patsubst)}
            echo SUBST: %(subst)s
        """ % locals())
AlwaysBuild(all)

参考にした文献

  1. http://www.scons.org/doc/HTML/scons-user/book1.html
  2. http://www.scons.org/doc/production/HTML/scons-man.html

とくに、User's Guideの最後のHandling Common Tasksやmanの最後の書き方の例は参考になる。

今回はMakefileと対応がとれるようにCommandのみを使ったが、Environmentを使って環境を使い分けたり、自分でビルダーを書いたり、オプションを使いなしたりすればより効率的な書き方ができるようになると思う。