Рубрики
Без рубрики

Как написать Makefile – Автоматизация настройки, компиляции и тестирования Python

В этом уроке мы рассмотрим основы Makefiles – regex, target notation и bash scripting. Мы напишем makefile для проекта Python, а затем выполним его с помощью утилиты make.

Автор оригинала: Vladimir Batoćanin.

Как написать 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.source и main_program.source , потому что мы хотим, чтобы наш проект был обновлен на всех концах.

Это очень утомительный и трудоемкий процесс. И поскольку все хорошее в мире программного обеспечения происходит от того, что какой-то инженер слишком ленив, чтобы ввести несколько дополнительных команд, Makefile родился.

Makefile использует утилиту make , и если быть абсолютно точным, Makefile-это просто файл, в котором содержится код, используемый утилитой make . Однако имя Makefile гораздо более узнаваемо.

Makefile по существу поддерживает ваш проект в актуальном состоянии, перестраивая только необходимые части исходного кода, чьи дочерние устарели. Он также может автоматизировать компиляцию, сборку и тестирование.

В этом контексте child – это библиотека или фрагмент кода, необходимый для запуска родительского кода.

Эта концепция очень полезна и обычно используется с скомпилированными языками программирования. Теперь вы, возможно, спрашиваете себя:

Разве Python не является интерпретируемым языком?

Ну, Python технически является одновременно интерпретируемым и компилируемым языком, потому что для того, чтобы он интерпретировал строку кода, он должен предварительно скомпилировать ее в байтовый код, который не жестко закодирован для конкретного процессора и может быть запущен после факта.

Более подробное, но краткое объяснение можно найти в блоге Неда Батчелдера . Кроме того, если вам нужно освежить знания о том, как работают процессоры языка программирования, мы вас накроем.

Разбивка концепции

Поскольку Makefile-это просто объединение нескольких концепций, есть несколько вещей, которые вам нужно знать, чтобы написать Makefile:

  1. Bash Scripting
  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, и вам не нужно будет писать его с нуля. Однако очень полезно понять синтаксис, если вы хотите его настроить.

Иногда изменение Make-файла по умолчанию даже является обязательным, например, когда вы хотите, чтобы OpenGL и Clean хорошо играли вместе.

Bash Scripting

Bash в основном используется для автоматизации дистрибутивов Linux и необходим для того, чтобы стать всемогущим “мастером”Linux. Это также императивный язык сценариев, что делает его очень читаемым и легким для понимания. Обратите внимание, что вы можете запустить bash в системах Windows, но на самом деле это не самый распространенный вариант использования.

Сначала давайте рассмотрим простую программу “Hello World” в Bash:

# 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

Это добавляет разрешение execute к целевому файлу. Однако, если вы хотите предоставить более конкретные разрешения, вы можете выполнить что-то похожее на следующую команду:

chmod 777 name_of_script.sh

Более подробная информация о chmod on this link .

Далее давайте быстро перейдем к некоторым основам использования простых 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 , это означает, что его логическое значение равно 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)

С другой стороны, вложенные выражения могут привести к кроличьей норе и могут стать чрезвычайно запутанными и нечитаемыми, поэтому не рекомендуется вкладывать более двух выражений.

Вас может смутить странная нотация [ -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, потому что разница в производительности в большинстве случаев незначительна.

Чаще всего Make-файлы используются для настройки проекта, очистки его, возможно, предоставления некоторой помощи и тестирования ваших модулей. Ниже приведен пример гораздо более реалистичного проекта 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:

запуск make с помощью makefile

Вывод

Makefile и make могут значительно облегчить вашу жизнь и могут использоваться практически с любой технологией или языком.

Он может автоматизировать большую часть вашего строительства и тестирования и многое другое. И как видно из приведенного выше примера, его можно использовать как с интерпретируемыми, так и с компилируемыми языками.