MakefileをPythonで置き換える¶
仕事でMakefileを書いていたのですが、ちょと複雑なことをしようとすると書き方を調べるのにやたらと時間を取られます。Pythonばっかり書いてるせいか、Makefileやshell scriptの書き方は毎回忘れますね。
MakefileをPythonで書ければ良いのに! とはいえ、ベタにPythonで書いて、ターゲット間の依存関係などを考え始めるとコードがめちゃくちゃ長くなってしまいます。ということでPythonでMakefileを置き換えるツールが無いか調べてみました。ざっと探して見つけたのが以下のツールです。
この中では、多分欲しいのはPaver辺りだろうなあと思いつつ、自分が欲しいのはどういう使い方なのかを把握するためにコードを書いてみました。
以下のようなmake.pyを記述できると、書くのが楽そうです。
# -*- coding: utf-8 -*-
from __future__ import absolute_import, print_function
import os
import sys
import time
import tarfile
from .mk import make
#defines
WORK_DIRS = ['bin', 'parts', 'develop-eggs']
EGG_DIR = 'eggs'
BUILDOUT_CMD = 'bin/buildout'
if sys.platform.startswith('win'):
BUILDOUT_CMD += '.exe'
@make.target() #this is make target.
@make.depend('buildout') #run dependents before target
def all():
return make.sh(BUILDOUT_CMD, '-N') #call shell command
@make.target(BUILDOUT_CMD) #check existent
def buildout():
make.mkdir(EGG_DIR) #make dir if not exist
return make.sh(
sys.executable, '-S', 'bootstrap.py',
'-d',
'--eggs', EGG_DIR,
'--setup-source', 'distribute_setup.py',
'--version', '1.5.2',
)
@make.target()
def clean():
make.rm(*WORK_DIRS) #remove directories if exist
@make.target()
def remove_eggs():
make.rm(EGG_DIR)
@make.target()
@make.depend('clean', 'remove_eggs', 'buildout')
def pkg():
make.sh(BUILDOUT_CMD, '-c', 'buildout-mkpkg.cfg')
make.call('clean') #call another make target
name = os.path.basename(os.getcwd())
filename = '{0}-{1}.tgz'.format(name, time.strftime('%Y%m%d-%H%M%S'))
with tarfile.open('../'+filename, 'w|gz') as tar:
tar.add('.', name)
print('deploy package:', filename)
if __name__ == '__main__':
make.run()
これを動かすためのmk.pyをざざっと実装してみました。
# -*- coding: utf-8 -*-
from __future__ import print_function
import os
import sys
import subprocess
import shutil
class Runner(object):
def __init__(self, func, depends=None, targets=None):
if isinstance(func, self.__class__):
self.func = func.func
self.depends = depends or func.depends or []
self.targets = targets or func.targets or []
else:
self.func = func
self.depends = depends or []
self.targets = targets or []
self.name = self.func.__name__
def __call__(self, *args, **kw):
# targets
if self.targets and __builtins__.all(
os.path.exists(target) for target in self.targets):
print("Skip:", self.name)
print(" all targets are exists:", self.targets)
return 0
# depends
for depend in self.depends:
ret = make.call(depend)
if ret:
return ret
# main
print("In:", self.name)
ret = self.func(*args, **kw)
print("Out:", self.name)
return ret
class make(object):
"""class for namespace"""
__commands = {}
#private
@classmethod
def _register(cls, func, depends=None, targets=None):
func = Runner(func, depends=depends, targets=targets)
cls.__commands[func.name] = func
return func
#decorator
@classmethod
def depend(cls, *depends):
def inner(func):
return cls._register(func, depends=depends)
return inner
#decorator
@classmethod
def target(cls, *targets):
def inner(func):
return cls._register(func, targets=targets)
return inner
#utility
@classmethod
def sh(cls, *args):
print(' '.join(args))
return subprocess.check_call(args)
#utility
@classmethod
def rm(cls, *dirs):
for d in dirs:
shutil.rmtree(d, True)
#utility
@classmethod
def mkdir(cls, path):
if not os.path.exists(path):
os.makedirs(EGG_DIR)
#utility
@classmethod
def call(cls, name, args=[], kw={}):
return cls.__commands[name](*args, **kw)
#utility
@classmethod
def run(cls):
if len(sys.argv) < 2:
if 'all' in cls.__commands:
sys.argv.append('all')
else:
print(sys.argv[0], 'need target.')
for key in sorted(cls.__commands.keys()):
print(' ', key)
sys.exit(-1)
target = sys.argv[1]
try:
ret = cls.call(target)
except:
print('Error in', target)
raise
if ret:
print('Error in', target, '::', ret)
if not isinstance(ret, int) and ret is not None:
ret = -1
sys.exit(ret)
なぜかclassmethodだらけ。使い方のわかりやすさ優先で実装したらなぜかこんなコードになりました。
これで以下のように実行できます。
$ python make.py all
...
$ python make.py pkg
...
deploy package: maketest-20120406190450.tgz
$
make all に比べると多少入力文字数が多くなりましたが、既存の手順を維持したいなら以下のようなMakefileを用意しておけば良いですね。
all:
python make.py all
pkg:
python make.py pkg
...
あとは、先のコードに近い記述ができるツール(=自分が使いやすいと感じるツール)を前述の4つから探せば良いわけですが、とりあえずの目的は達成しちゃったので調べるのはまた今度にします。
ソースコード¶
作ったコードは以下から取得出来ます。