天狗会議録
Posts Pages About
MakeのようなPythonデコレータ

動機

C/C++プロジェクトの開発環境として次の候補が挙げられます:

私は以下の理由でConan+Mesonを採用しています:

ConanとMesonはPython製であるため、C/C++プロジェクトと言いつつもPython環境が必須となります。 それはそれで、クロスプラットフォームなビルド時処理の記述言語としてPythonを採用できるようになったので悪いことではありませんが。 Windowsは何をインストールするにせよ面倒である・かつ環境の汚れが気になるOSであるため、このPythonをインストールするのも一苦労であるという汚点があります。 uvはこのソリューションとなります。 uvを使えばPythonだけでなくConanもMesonもスマートに導入・管理できるのです。

ところで、Conan+Mesonを採用していても、C/C++のビルドコマンドは複雑なものになります。 これを緩和するために次の解決策が考えられます:

バッチファイルやシェルスクリプトが愚策であることは言うまでもありません。 複雑なコマンドを実行するならば、Pythonスクリプトはあっという間に汚くなってしまうでしょう。 従って、タスクランナーを導入することになります。 まず、タスクランナーとしてMakeを採用することが考えられます。 macOSとLinuxという、Makeの導入が容易で・かつ基礎コマンドの多くを共有している環境を想定するならばMakeで十分です。 しかし、Windowsも想定するならば、Makeのインストールが一苦労である上に・UNIX系と混在させるためにMakeから逐一Pythonスクリプトを呼ぶという滑稽な状況に陥ってしまいます。 Makeの代替であるjustでも同様です。

当然ながら、この世にはuvでインストールできるタスクランナーがあります:

しかし、doitを除く二つはあくまでタスクランナーであり、依存関係を追跡・解決する機能を持ちません。 また、doitはその機能を持ちますが、現在メンテナンスされているとは言い難い状況にあります。 すぐに破壊的変更をするPythonという言語においてメンテナンスされていないものを使いたいとは積極的には思えません。

そこで、Makeのような依存関係を追跡・解決する機能を持つPythonスクリプトを自力実装して、上記タスクランナーと併用することにしました。

実装

次のようにデコレータtaskを定義します:

# scripts/make.py
import inspect
import subprocess
import sys
from functools import wraps
from pathlib import Path

def task(trg=None, deps=None):
	def decorator(func):
		@wraps(func)
		def wrapper():
			if deps:
				for d in deps:
					if callable(d):
						d()

			func_param_count = len(inspect.signature(func).parameters)

			if func_param_count == 0:
				ret = lambda: func()
			elif func_param_count == 1:
				ret = lambda: func(trg)
			else:
				ret = lambda: func(trg, deps)

			if trg is None:
				return ret()

			trg_path = Path(trg)

			if not trg_path.exists():
				return ret()

			dep_paths = [Path(d) for d in (deps or []) if isinstance(d, str)]

			for d in dep_paths:
				if not d.exists():
					print(f"error: task dependency not found: {d}")
					sys.exit(1)

			trg_mtime = trg_path.stat().st_mtime

			if any(d.stat().st_mtime > trg_mtime for d in dep_paths):
				return ret()

			print(f"{trg} is up to date")

		return wrapper
	return decorator

使用方法

次のように使用します:

# scripts/tasks.py
from . import conan
from . import meson
from .make import task

@task("conan-deps/.stamp", ["conanfile.py"])
def install_dependencies(trg):
	conan.install()
	Path(trg).touch()

@task("build/.stamp")
def setup_build(trg):
	meson.setup()
	Path(trg).touch()

@task(None, [install_dependencies, setup_build])
def build():
	meson.compile()

これはMakefileでいう次と同じ動作をします(一部コマンドは簡略化):

conan-deps/.stamp: conanfile.py
	conan install .
	touch $@

build/.stamp:
	meson setup build
	touch $@

.PHONY: build
build: conan-deps/.stamp build/.stamp
	meson compile -C build

例えば、taskipyを使うなら、次をpyproject.tomlに書き:

[tool.taskipy.tasks]
build = "python -c \"from scripts.tasks import build; build()\""

次のように呼び出します:

uv run task build

雑記