Makefile を使った問題作成の方法

Makefile の概要

Makefile とは何か Makefile は、GNU make ツールの動作を制御するファイルです。GNU make はプロジェクトのビルドロジックを管理するツールで、複雑な依存関係を持つプロジェクトの効率的なビルドを可能にします。

Makefile の基本構文 以下のような単純なプロジェクトを使用して、Makefile の基本構文を説明します。

example-project/
├── build/
├── src/
│   ├── lib1.cpp
│   ├── lib1.h
│   ├── lib2.cpp
│   ├── lib2.h
│   └── main.cpp
└── Makefile

目標の定義 Makefile では、目標(target)という抽象概念を使用してプロジェクトを構築します。目標はディスク上のファイルパスであり、そのファイルが存在すれば目標が構築されたとみなされます。

build/main: src/main.cpp src/lib1.h src/lib2.h src/lib1.cpp src/lib2.cpp
	g++ src/main.cpp src/lib1.cpp src/lib2.cpp -I src -o build/main

依存関係の組織 より構造化されたプロジェクトのために、以下のコードのように変更できます。

build/lib1.o: src/lib1.cpp src/lib1.h
	g++ src/lib1.cpp -I src -c -o build/lib1.o

build/lib2.o: src/lib2.cpp src/lib2.h
	g++ src/lib2.cpp -I src -c -o build/lib2.o

build/main: build/lib1.o build/lib2.o src/main.cpp
	g++ build/lib1.o build/lib2.o src/main.cpp -I src -o build/main

環境変数の定義 Makefile では変数を定義することができます。

CXX = g++
CXX_FLAGS = -std=c++20 -O2 -I $(SRC_DIR)

BUILD_DIR = build
SRC_DIR = src

LIB1 = $(BUILD_DIR)/lib1.o
LIB2 = $(BUILD_DIR)/lib2.o
MAIN = $(BUILD_DIR)/main

$(LIB1): $(SRC_DIR)/lib1.cpp $(SRC_DIR)/lib1.h
	$(CXX) $(CXX_FLAGS) $< -c -o $@

$(LIB2): $(SRC_DIR)/lib2.cpp $(SRC_DIR)/lib2.h
	$(CXX) $(CXX_FLAGS) $< -c -o $@

$(MAIN): $(LIB1) $(LIB2) $(SRC_DIR)/main.cpp
	$(CXX) $(CXX_FLAGS) $^ -o $@

パターンマッチング Makefile では、% を使用したパターンマッチングが可能です。

build/%.o: src/%.cpp
	echo Compiling src/$*.cpp
	$(CXX) $(CXX_FLAGS) -c -o $@

関数呼び出し Makefile では、いくつかの組み込み関数を使用できます。

define BUILD_SINGLE_POINT
TEST_POINTS += $(DATA_DIR)/subtask-$(1)/$(2).in $(DATA_DIR)/subtask-$(1)/$(2).ans

$(DATA_DIR)/subtask-$(1)/$(2).in: $(GEN)
	mkdir -p $$(@D)
	$(GEN) $(3) $(shell echo $(1),$(2) | md5sum | cut -d ' ' -f 1) > $$@
	$(VAL) < $$@

$(DATA_DIR)/subtask-$(1)/$(2).ans: $(DATA_DIR)/subtask-$(1)/$(2).in $(STD)
	$(STD) < $$< > $$@
endef

偽目標 偽目標は、実際のファイルが存在しなくても構築される目標です。

.PHONY: clean
clean:
	rm -rf $(BUILD_DIR)/*

Makefile を使った問題作成

前提条件 Linux または WSL で作業することを想定しています。

問題作成の手順 問題作成には以下の手順があります。

  1. 問題文と解説を書く。
  2. 標準解答を書く。
  3. ジェネレータを書く。
  4. バリデータを書く。
  5. データを生成する。

Makefile の作成

変数の定義 まずは変数を定義します。

CXX = g++
CXX_OPTIONS = -std=c++20 -O2
SRC_DIR = src
BUILD_DIR = build
DATA_DIR = $(BUILD_DIR)/data

STD = $(BUILD_DIR)/std
GEN = $(BUILD_DIR)/gen
VAL = $(BUILD_DIR)/val
DOCS = $(patsubst %,$(BUILD_DIR)/%,std.cpp gen.cpp val.cpp statement.md solution.md)
TEST_POINTS := 

依存関係と生成ルールの記述 次に、依存関係と生成ルールを記述します。

$(DOCS): $(BUILD_DIR)/%: $(SRC_DIR)/%
	cp $< $@

$(GEN) $(STD) $(VAL): $(BUILD_DIR)/%: $(SRC_DIR)/%.cpp
	$(CXX) $(CXX_OPTIONS) $< -o $@

テストケースの生成 テストケースの生成には、以下の関数を使用します。

define BUILD_SINGLE_POINT
TEST_POINTS += $(DATA_DIR)/subtask-$(1)/$(2).in $(DATA_DIR)/subtask-$(1)/$(2).ans

$(DATA_DIR)/subtask-$(1)/$(2).in: $(GEN)
	mkdir -p $$(@D)
	$(GEN) $(3) $(shell echo $(1),$(2) | md5sum | cut -d ' ' -f 1) > $$@
	$(VAL) < $$@

$(DATA_DIR)/subtask-$(1)/$(2).ans: $(DATA_DIR)/subtask-$(1)/$(2).in $(STD)
	$(STD) < $$< > $$@
endef

define BUILD_SUBTASK
$(foreach i,$(shell seq 1 $(2)),$(eval $(call BUILD_SINGLE_POINT,$(1),$(i),$(3))))
endef

$(eval $(call BUILD_SUBTASK,1,5,--id=1))
$(eval $(call BUILD_SUBTASK,2,5,--id=2))
$(eval $(call BUILD_SUBTASK,3,10,--id=3 --special-property))
$(eval $(call BUILD_SUBTASK,4,10,--id=4))
$(eval $(call BUILD_SUBTASK,5,15,--id=5))

特別な目標の設定 最後に、特別な目標を設定します。

.DEFAULT_GOAL := all

all: $(TEST_POINTS) $(DOCS) $(GEN) $(STD) $(VAL)
clean:
	rm -rf $(BUILD_DIR)/*

.PHONY: all clean
.DELETE_ON_ERROR: $(TEST_POINTS)

完全な Makefile 以上の内容を統合すると、次のようになります。

CXX = g++
CXX_OPTIONS = -std=c++20 -O2
SRC_DIR = src
BUILD_DIR = build
DATA_DIR = $(BUILD_DIR)/data

STD = $(BUILD_DIR)/std
GEN = $(BUILD_DIR)/gen
VAL = $(BUILD_DIR)/val
DOCS = $(patsubst %,$(BUILD_DIR)/%,std.cpp gen.cpp val.cpp statement.md solution.md)
TEST_POINTS := 

define BUILD_SINGLE_POINT
TEST_POINTS += $(DATA_DIR)/subtask-$(1)/$(2).in $(DATA_DIR)/subtask-$(1)/$(2).ans

$(DATA_DIR)/subtask-$(1)/$(2).in: $(GEN)
	mkdir -p $$(@D)
	$(GEN) $(3) $(shell echo $(1),$(2) | md5sum | cut -d ' ' -f 1) > $$@
	$(VAL) < $$@

$(DATA_DIR)/subtask-$(1)/$(2).ans: $(DATA_DIR)/subtask-$(1)/$(2).in $(STD)
	$(STD) < $$< > $$@
endef

define BUILD_SUBTASK
$(foreach i,$(shell seq 1 $(2)),$(eval $(call BUILD_SINGLE_POINT,$(1),$(i),$(3))))
endef

.DEFAULT_GOAL := all

$(DOCS): $(BUILD_DIR)/%: $(SRC_DIR)/%
	cp $< $@

$(GEN) $(STD) $(VAL): $(BUILD_DIR)/%: $(SRC_DIR)/%.cpp
	$(CXX) $(CXX_OPTIONS) $< -o $@

$(eval $(call BUILD_SUBTASK,1,5,--id=1))
$(eval $(call BUILD_SUBTASK,2,5,--id=2))
$(eval $(call BUILD_SUBTASK,3,10,--id=3 --special-property))
$(eval $(call BUILD_SUBTASK,4,10,--id=4))
$(eval $(call BUILD_SUBTASK,5,15,--id=5))

all: $(TEST_POINTS) $(DOCS) $(GEN) $(STD) $(VAL)
clean:
	rm -rf $(BUILD_DIR)/*

.PHONY: all clean
.DELETE_ON_ERROR: $(TEST_POINTS)

タグ: Makefile C++ GNU make OI Problem Setting

7月11日 20:39 投稿