Makefileの書き方 – Pythonのセットアップ、コンパイル、テストの自動化

複数のソースやリソースなどを持つプロジェクトを実行する場合、メインプログラムのコンパイルや実行の前に、すべてのコードが再コンパイルされることを確認する必要があります。

例えば、私たちのソフトウェアが次のようなものだと想像してください。

main_program.source -> uses the libraries `math.source` and `draw.source`
math.source -> uses the libraries `floating_point_calc.source` and `integer_calc.source`
draw.source -> uses the library `opengl.source`


例えば、opengl.sourceに変更を加えると、draw.sourcemain_program.sourceの両方を再コンパイルする必要があるわけで、プロジェクトのすべてのエンドで最新の状態にしたいからです。

これは非常に面倒で時間のかかる作業です。

そして、ソフトウェアの世界におけるすべての良いことは、余分なコマンドを入力するのが面倒なエンジニアから生まれるので、Makefileが生まれました。

Makefile は make ユーティリティを使用しますが、正確に言えば、Makefile は make ユーティリティが使用するコードを格納したファイルに過ぎません。

しかし、Makefileという名前の方が、ずっと認知度が高いのです。

Makefile は基本的に、ソースコードの children が古くなったときに必要な部分だけを再構築することで、プロジェクトを最新の状態に保つことができます。

また、コンパイル、ビルド、テストを自動化することもできます。

この文脈では、childrenとは、親コードを実行するために不可欠なライブラリやコードの塊のことを指します。

この概念は非常に便利で、コンパイルされたプログラミング言語でよく使われます。

さて、あなたは自分自身に問いかけているかもしれません。

Pythonはインタプリタ型言語ではないのですか?
という疑問があると思います。

という疑問があるかもしれません。

Pythonは技術的にはインタプリタ言語とコンパイル言語の両方です。

なぜなら、コード行を解釈するためには、それを特定のCPU用にハードコードされていないバイトコードにプリコンパイルする必要があり、事後的に実行することが可能だからです。

より詳細で簡潔な説明は、Ned Batchelderのブログで見ることができる。

また、プログラミング言語プロセッサの仕組みについて復習が必要な場合は、こちらもご参照ください。

コンセプトブレイクダウン

Makefile は複数の概念の集合体に過ぎないので、Makefile を書くために知っておく必要があることがいくつかあります。

  1. Bash スクリプト
  2. 正規表現
  3. ターゲット表記
  4. プロジェクトのファイル構造を理解する

これらを手に入れれば、makeユーティリティの命令を書き、コンパイルを自動化することができるようになります。

Bashはコマンド言語(Unixシェルでもありますが、今はあまり関係ありません)であり、実際にコマンドを書いたり、ファイル生成を自動化するために使用します。

例えば、全てのライブラリ名をユーザーにエコーする場合。

DIRS=project/libs
for file in $(DIRS); do
    echo $$file
done


ターゲット表記は、どのファイルが他のファイルに依存しているかを書く方法である。

例えば、上の例で出てきた依存関係を適切なターゲット表記で表現したい場合は、次のように書きます。

main_program.cpp: math.cpp draw.cpp
math.cpp: floating_point_calc.cpp integer_calc.cpp
draw.cpp: opengl.cpp


ファイル構造に関しては、あなたのプログラミング言語と環境に依存します。

IDEによってはMakefileのようなものを自動的に生成してくれるものもあり、一から書く必要はないでしょう。

しかし、構文を理解することは、それを微調整したい場合にとても役に立ちます。

時には、デフォルトのMakefileを変更することが必須となることもあります。

例えば、OpenGLとCLionを一緒にうまく動作させたい場合などです。

Bashスクリプト

BashはLinuxディストリビューションで自動化のために使われることが多く、万能のLinux「ウィザード」になるには欠かせません。

また、命令型のスクリプト言語であるため、非常に読みやすく、理解しやすい言語です。

Windowsシステムでもbashを実行できますが、あまり一般的な使用例ではありません。

まず、Bashで簡単な「Hello World」プログラムを作ってみましょう。

# Comments in bash look like this


#!/bin/bash
# The line above indicates that we'll be using bash for this script
# The exact syntax is: #![source]
echo "Hello world!"


スクリプトを作成するとき、現在の umask の状態によっては、スクリプト自体が実行できないかもしれません。

この場合、ターミナルで次のコードを実行することで変更できます。

chmod +x name_of_script.sh


これにより、対象ファイルに実行権限が追加されます。

しかし、より具体的なパーミッションを与えたい場合は、以下のコマンドのようなものを実行します。

chmod 777 name_of_script.sh


chmod` の詳細については、このリンクを参照してください。

次に、簡単な if 文と変数を使った基本的な操作について説明します。

#!/bin/bash


echo "What's the answer to the ultimate question of life, the universe, and everything?"
read -p "Answer: " number
# We dereference variables using the $ operator
echo "Your answer: $number computing..."
# if statement
# The double brackets are necessary, whenever we want to calculate the value of an expression or subexpression, we have to use double brackets, imagine you have selective double vision.
if (( number == 42 ))
then
    echo "Correct!"
    # This notation, even though it's more easily readable, is rarely used.
elif (( number == 41 || number == 43 )); then
    echo "So close!"
    # This is a more common approach
else
    echo "Incorrect, you will have to wait 7 and a half million years for the answer!"
fi


さて、フロー制御を記述する方法として、if文とは別の方法があり、実はそちらの方がより一般的です。

ご存知のように、ブール演算子は副作用を発生させるためだけに使われることがあり、次のようなものがあります。

++a && b++


つまり、まず a をインクリメントして、使っている言語によっては、式の値が True と評価されるかどうかをチェックします(一般に、整数が >0 または =/=0 であれば、その boolean 値が True であることを意味します)。

そして、もし True ならば、 b をインクリメントします。

この概念は条件付き実行と呼ばれ、bashスクリプトなどで非常によく使われます。

#!/bin/bash


# Regular if notation
echo "Checking if project is generated..."
# Very important note, the whitespace between `[` and `-d` is absolutely essential
# If you remove it, it'll cause a compilation error
if [ -d project_dir ]
then
    echo "Dir already generated."
else
    echo "No directory found, generating..."
    mkdir project_dir
fi


これは条件付き実行を使って書き直すことができる。

echo "Checking if project is generated..."
[ -d project_dir ] || mkdir project_dir


また、入れ子式を使ってさらに詳しく書くこともできます。

echo "Checking if project is generated..."
[ -d project_dir ] || (echo "No directory found, generating..." && mkdir project_dir)


しかし、式のネストはウサギの穴につながる可能性があり、非常に複雑で読みにくいものになるので、最大でも2つ以上の式をネストすることはお勧めしません。

上のコードで使われている [ -d ] という奇妙な記法に戸惑うかもしれませんが、それはあなただけではありません。

その理由は、もともとBashの条件文は test [EXPRESSION] コマンドで書かれていたからです。

しかし、人々が条件式を括弧で書き始めたとき、Bashは非常に無頓着なハックではありますが、[文字をtestコマンドにリマップし、]は式の終わりを意味する、おそらく事後的に実装されたものです。

このため、test -d FILENAMEというコマンドを使うことができます。

これは、[ -d FILENAME ]のように、与えられたファイルが存在し、ディレクトリであるかどうかをチェックするコマンドです。

正規表現

正規表現(regex)は、コードを一般化する簡単な方法を提供します。

あるいは、特定の条件を満たす特定のファイルのサブセットに対してアクションを繰り返すこともできます。

以下のコードスニペットで、正規表現の基礎といくつかの例について説明します。

注:ある表現が単語を捕らえる( – )というのは、指定された単語が正規表現で定義された単語の部分集合に含まれることを意味します。

# Literal characters just signify those same characters
StackAbuse -> StackAbuse
sTACKaBUSE -> sTACKaBUSE


# The or (|) operator is used to signify that something can be either one or other string
Stack|Abuse -> Stack
            -> Abuse
Stack(Abuse|Overflow) -> StackAbuse
                      -> StackOverflow


# The conditional (?) operator is used to signify the potential occurrence of a string
The answer to life the universe and everything is( 42)?...
    -> The answer to life the universe and everything is...
    -> The answer to life the universe and everything is 42...

# The * and + operators tell us how many times a character can occur
# * indicates that the specified character can occur 0 or more times
# + indicates that the specified character can occur 1 or more times 
He is my( great)+ uncle Brian. -> He is my great uncle Brian.
                               -> He is my great great uncle Brian.
# The example above can also be written like this:
He is my great( great)* uncle Brian.


これは、Makefile を使って当面必要な最低限のことです。

しかし、長期的に見れば、正規表現を学ぶことは本当に良い考えです。

ターゲット表記

ここまできたら、いよいよMakefileの構文の本題に入りましょう。

ターゲット表記は、ソースファイル間に存在するすべての依存関係を表現する方法にすぎません。

冒頭の例と同じファイル構成の例を見てみましょう。

# First of all, all pyc (compiled .py files) are dependent on their source code counterparts
main_program.pyc: main_program.py
    python compile.py $<
math.pyc: math.py
    python compile.py $< 
draw.pyc: draw.py
    python compile.py $<


# Then we can implement our custom dependencies
main_program.pyc: main_program.py math.pyc draw.pyc
    python compile.py $<
math.pyc: math.py floating_point_calc.py integer_calc.py
    python compile.py $< 
draw.pyc: draw.py opengl.py
    python compile.py $<


上記はターゲット表記がどのように機能するかを明確にするためのものであることに留意してください。

このようなPythonのプロジェクトでは、性能の違いはほとんどの場合無視できるので、ほとんど使われることはありません。

それよりも、Makefile はプロジェクトのセットアップやクリーンアップ、ヘルプの提供、モジュールのテストに使われることが多いようです。

以下は、より現実的な Python プロジェクトの Makefile の例です。

# Signifies our desired python version
# Makefile macros (or variables) are defined a little bit differently than traditional bash, keep in mind that in the Makefile there's top-level Makefile-only syntax, and everything else is bash script syntax.
PYTHON = python3


# .PHONY defines parts of the makefile that are not dependant on any specific file
# This is most often used to store functions
.PHONY = help setup test run clean


# Defining an array variable
FILES = input output


# Defines the default target that `make` will to try to make, or in the case of a phony target, execute the specified commands
# This target is executed whenever we just type `make`
.DEFAULT_GOAL = help


# The @ makes sure that the command itself isn't echoed in the terminal
help:
    @echo "---------------HELP-----------------"
    @echo "To setup the project type make setup"
    @echo "To test the project type make test"
    @echo "To run the project type make run"
    @echo "------------------------------------"


# This generates the desired project file structure
# A very important thing to note is that macros (or makefile variables) are referenced in the target's code with a single dollar sign ${}, but all script variables are referenced with two dollar signs $${}
setup:

    @echo "Checking if project files are generated..."
    [ -d project_files.project ] || (echo "No directory found, generating..." && mkdir project_files.project)
    for FILE in ${FILES}; do 
        touch "project_files.project/$${FILE}.txt"; 
    done


# The ${} notation is specific to the make syntax and is very similar to bash's $() 
# This function uses pytest to test our source files
test:
    ${PYTHON} -m pytest

run:
    ${PYTHON} our_app.py


# In this context, the *.project pattern means "anything that has the .project extension"
clean:
    rm -r *.project


それでは、ターミナルを開いて Makefile を実行し、Python プロジェクトを生成してコンパイルしてみましょう。

結論

Makefile と make はあなたの生活をずっと楽にしてくれますし、ほとんどすべての技術や言語で使うことができます。

ビルドやテストのほとんどを自動化することができますし、その他にも多くのことができます。

そして、上の例からわかるように、インタプリタ言語とコンパイル言語の両方で使用することができます。

タイトルとURLをコピーしました