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

Введение Если вы хотите запустить проект, который имеет несколько источников, ресурсов и т. Д., Вам необходимо убедиться, что весь код перекомпилирован, прежде чем основная программа будет скомпилирована или запущена. Например, представьте, что наше программное обеспечение выглядит примерно так: main_program.source -> использует библиотеки `math.source` и` draw.source` math.source -> использует библиотеки `Floating_point_calc.source` и` integer_calc.source` draw .source -> использует библиотеку `opengl.source` Итак, если мы внесем изменения в opengl.so

Вступление

Если вы хотите запустить проект, который имеет несколько источников, ресурсов и т. Д., Вам необходимо убедиться, что весь код перекомпилирован перед компиляцией или запуском основной программы.

Например, представьте, что наше программное обеспечение выглядит примерно так:

 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:

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

запуск make с помощьюmake-файла{.ezlazyload}

Заключение

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

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

comments powered by Disqus