Вступление
Если вы хотите запустить проект, который имеет несколько источников, ресурсов и т. Д., Вам необходимо убедиться, что весь код перекомпилирован перед компиляцией или запуском основной программы.
Например, представьте, что наше программное обеспечение выглядит примерно так:
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 по существу поддерживает ваш проект в актуальном состоянии,
перестраивая только необходимые части исходного кода, children
элементы которых устарели. Он также может автоматизировать компиляцию,
сборки и тестирование.
В этом контексте
child
элемент - это библиотека или фрагмент кода, который необходим для запуска родительского кода.
Эта концепция очень полезна и обычно используется с компилируемыми языками программирования. Теперь вы можете спросить себя:
Разве Python не интерпретируемый язык?
Что ж, Python технически является одновременно интерпретируемым и компилируемым языком, потому что для того, чтобы он интерпретировал строку кода, ему необходимо предварительно скомпилировать его в байтовый код, который не жестко запрограммирован для конкретного процессора и может быть запущен постфактум.
Более подробное, но краткое объяснение можно найти в блоге Неда Батчелдера . Кроме того, если вам нужно вспомнить, как работают процессоры языков программирования , мы вам поможем.
Разбивка концепции
Поскольку Makefile представляет собой просто объединение нескольких концепций, вам нужно знать несколько вещей, чтобы написать Makefile:
- Сценарии Bash
- Регулярные выражения
- Целевая нотация
- Понимание файловой структуры вашего проекта
Имея их в руках, вы сможете писать инструкции для 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. Это также императивный язык сценариев, что делает его очень читабельным и легким для понимания. Обратите внимание, что вы можете запускать 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
Это добавляет разрешение на выполнение для целевого файла. Однако, если вы хотите предоставить более конкретные разрешения, вы можете выполнить что-то похожее на следующую команду:
chmod 777 name_of_script.sh
Больше информации о chmod
по этой
ссылке .
Далее, давайте быстро перейти на некоторые основы с использованием
простой , if
-statements и переменные:
#!/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)
С другой стороны, вложенные выражения могут привести к кроличьей норе и могут стать чрезвычайно запутанными и нечитаемыми, поэтому не рекомендуется вкладывать более двух выражений максимум.
Вы можете быть сбиты с толку странной [ -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 используются для настройки проекта, его очистки, возможно, для оказания помощи и тестирования ваших модулей. Ниже приведен пример гораздо более реалистичного проекта Makefile Python:
# 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:
{.ezlazyload}
Заключение
Makefile и make могут значительно облегчить вашу жизнь и могут использоваться практически с любыми технологиями и языками.
Он может автоматизировать большую часть вашего строительства, тестирования и многое другое. И, как видно из приведенного выше примера, его можно использовать как с интерпретируемыми, так и с компилируемыми языками.