Метапрограммирование

Что авторы курса подразумевают под “метапрограммированием”? Оказалось, что это лучший из предложенных терминов для описания процесса разработки в общем, чем написания кода и эффективной работы. В этой лекции мы рассмотрим инструменты для создания и тестирования кода и управления зависимостями. Может показаться, что эти вещи играют совсем не ключевую роль в повседневной жизни студента. Но в стажировке и будущей работе полученные знания пригодятся. Обращаем внимание, что у метапрограммирования есть и другое общепринятое значение “вид программирования, связанный с созданием программ, которые порождают другие программы как результат своей работы”, которое отличается от того, что мы имеем в виду в лекции.

Системы сборки

Для большинства проектов, содержат они код или нет (например, LaTeX), существует процесс сборки. Это некоторый пошаговый набор действий обязательный для получения финального результата. Этот процесс часто состоит из множества этапов. Для получения окончательного результата нужно сначала запустить одно, потом сделать другое, переделать черновик, улучшить/изменить. Весь процесс довольно долгий и раздражающий, но, как и для многих вещей в этом курсе, уже созданы инструменты, облегчающие работу.

Они многочисленны и обычно называются системами сборки. Какую выбрать, зависит от поставленной задачи, языка программирования, размера проекта. Хотя в принципе все системы похожи между собой. Вы определяете набор зависимостей (dependencies), количество этапов/таргетов (targets, это названия/имена того, что надо собирать) и правил (rules). Вы сообщаете системе сборки конкретный таргет, а ее задача - найти все транзитивные (непрямые) зависимости, а затем применить правила для создания промежуточных таргетов, пока не будет исполнен окончательный таргет. В идеале системы сборки делают это без лишнего применения правил для таргегов, зависимости которых не изменились и чей результат доступен из предыдущего билда.

make одна из самых распространенных систем сборки, установленная на большинстве UNIX машин. У нее есть свои недостатки, но она хорошо работает для простых, средних и сложных проектов. Когда вы запускаете make, она обращается к файлу Makefile в текущей директории. Все таргеты, их зависимости и правила определены в этом файле:

paper.pdf: paper.tex plot-data.png
	pdflatex paper.tex

plot-%.png: %.dat plot.py
	./plot.py -i $*.dat -o $@

Каждая директива в этом файле - это правило по созданию левой части с помощью правой. Иначе говоря, то, что справа - это зависимости, а слева - таргеты. Блок с отступом - это серия программ, которые производят таргеты из этих зависимостей. В make первая директива определяет цель по умолчанию. Если make запустить без аргументов, то будут собираться все файлы сразу. Поэтому, если нужно собрать конкретный подпроект, требуется уточнение, например, make plot-data.png.

Символ % содержится в паттернах, он показывает любую подстроку для сбора таргета. Например, если таргет plot-foo.png, то make будет искать зависимости foo.dat и plot.py. Посмотрим, что будет, если запустить make с пустой исходной директорией:

$ make
make: *** No rule to make target 'paper.tex', needed by 'paper.pdf'.  Stop.

make говорит нам о том, что для сборки paper.pdf нужен paper.tex. Но как создать этот требуемый файл, не указано. Давайте попробуем его создать:

$ touch paper.tex
$ make
make: *** No rule to make target 'plot-data.png', needed by 'paper.pdf'.  Stop.

Так, интересно. Есть правило для создания plot-data.png, но это правило паттерна. До тех пор, пока файла (foo.dat) не существует, make не может его создать. Тогда все файлы создадим мы:

$ cat paper.tex
\documentclass{article}
\usepackage{graphicx}
\begin{document}
\includegraphics[scale=0.65]{plot-data.png}
\end{document}
$ cat plot.py
#!/usr/bin/env python
import matplotlib
import matplotlib.pyplot as plt
import numpy as np
import argparse

parser = argparse.ArgumentParser()
parser.add_argument('-i', type=argparse.FileType('r'))
parser.add_argument('-o')
args = parser.parse_args()

data = np.loadtxt(args.i)
plt.plot(data[:, 0], data[:, 1])
plt.savefig(args.o)
$ cat data.dat
1 1
2 2
3 3
4 4
5 8

Теперь что будет, если мы запустим make?

$ make
./plot.py -i data.dat -o plot-data.png
pdflatex paper.tex
... lots of output ...

PDF создан! А если еще раз запустить make?

$ make
make: 'paper.pdf' is up to date.

Ничего не произошло! Но системе сборки ничего и не нужно было делать. Она просто проверила все ранее созданные билды и зависимости. Ошибок не обнаружено. Мы можем немного изменить paper.tex и перезапустить make:

$ vim paper.tex
$ make
pdflatex paper.tex
...

Заметьте, что make не перезапустил plot.py, потому что это не нужно, так как ни одна из зависимостей plot-data.pngне изменилась.

Система зависимостей

На более высоком макро уровне, выши проекты скорее всего будут иметь зависимости, которые сами по себе будет проектами. Это может зависеть от установленных программ (например, python), системных пакетов (openssl) или библиотек языка программирования (matplotlib). Сегодня большинство зависимостей доступны через репозиторий, который содержит большое количество таких зависимостей в одном месте и предоставляет удобный механизм для их установки. Это, например, RubyGems для библиотек Ruby, PyPi для библиотек Python или репозиторий Arch для Arch Linux.

Поскольку точный механизм взаимодействия с разными репозиториями сильно различается, мы не будем сейчас вдаваться в детали какого-либо конкретного репозитория. Рассмотрим некоторые из наиболее распространенных терминов, которые все они используют. Первый из них - это версии. Большинство проектов содержит номер версии, обновляемый с каждым новым релизом. Обычно он выглядит как-то так: 8.1.3 или 64.1.20192004. Обычно, но не всегда, версиям присваиваются номера.
Номера нужны по разным причинам, основная - обеспечение работы программ. Представьте, например, что выпущена новая версия библиотеки, в которой переименована одна функция. Если кто-то попытается собрать программу, которая зависит от этой библиотеки, после того, как было выпущено обновление, сборка может завершиться ошибкой, потому что вызываемая функция больше не существует! Версии пытаются решить эту проблему, потому что указывают, что проект зависит от конкретной версии. Таким образом, даже если библиотека изменится, программное обеспечение, зависящее от нее, продолжит сборку с использованием старой версии библиотеки.

Это тоже не идеальное решение! Что делать, если сделать обновление безопасности, которое не меняет публичный интерфейс этой описанной выше библиотеки (ее API). И новую версию с обновленными параметрами безопасности теперь необходимо использовать во всех программах, которые зависели от старой версии. Здесь нужно смотреть на разные группы чисел в версиях. Точное значение каждого из них зависит от проекта, но один относительно общий стандарт - это семантическое версионирование semantic versioning. С таким подходом, номер каждой версии выглядит как: мажорная.минорная.патч версия (major.minor.patch). Учитывая номер версии МАЖОРНАЯ.МИНОРНАЯ.ПАТЧ, следует увеличивать:

Такая нумерация дает преимущества. Если мой проект использовал при сборке чужую библиотеку с версией 1.3.7, я все еще могу использовать версии с 1.3.8, 1.6.1 или даже 1.3.0. Версия 2.2.4, вероятно, уже не подходит, поскольку мажорная версия увеличилась. Пример семантического версионирования - версии Python. Многие из вас могли заметить, что версии Python 2 and Python 3 не согласуются между собой, как раз потому что изменилась мажорная версия. А код, написанные на Python 3.5 нормально запускается и на Python 3.7.

Работая с системами версий, вы могли заметить lock файлы. Это файлы, кторые содержат в себе полную информацию обо всех установленных зависимостях, включая их точные версии. Обычно необходимо вручную запустить обновление программы, чтобы обновить зависимости до более новых версий. На это есть много причин, таких как избежание перекомпиляции, детерминированной (воспроизводимой) компиляции или невозможность автоматического обновления до последней версии (которая может быть повреждена).

Continuous integration systems (CI) или тестирующие системы

С ростом проекта будет возникать больше дополнительных задач, которые нужно выполнять всякий раз при внесении изменений. Возможно, потребуется обновить новую версию документации, загрузить куда-нибудь скомпилированную версию, опубликовать код в pypi, выполнить тестирование и многое другое. Может быть, каждый раз, когда кто-то отправляет пуллреквест в GitHub, вы хотите проверить его код? На помощь к автоматизации таких процессов приходят тестирующие системы (CI).

CI (continuous integration) системы обозначают “запускаем что-то при любом изменении кода”. Есть много разных, обычно опен-сорсных, и бесплатных CI. Наиболее крупные и известные: Travis CI, Azure Pipelines и GitHub Actions. Все они работают по похожему сценарию: при любом изменении кода запускаются тесты.

Коротко о тестировании

Большинство крупных программных проектов поставляются с «набором тестов». Вы уже знакомы с общей концепцией тестирования, но ниже перечислены некоторые подходы к тестированию и терминология, которые вы можете встретить в работе:

Упражнения

  1. Большинство makefiles предоставляют таргет (цель) clean. Он не предназначен для создания файла clean, а вместо этого удаляет файлы, которые могут быть пересобраны make. Считайте, что это способ “отменить” все шаги сборки. Используйте таргет clean для сборки paper.pdf из Makefile выше. Создайте таргет clean для приведенного выше файла paper.pdf. Вам необходимо сделать таргет phony. Может пригодиться подкоманда git ls-files. Некоторые другие, широко используемые таргеты, перечислены здесь.
  2. Обратите внимание на разные способы определить требуемую версию зависимостей в системе сборки Rust. Большинство репозиториев поддерживают похожий синтаксис. Для каждого из вариантов (caret, tilde, wildcard, comparison, and multiple) постарайтесь придумать сценарий использования, в котором такое указание версии зависимости будет иметь смысл.
  3. Git может сам по себе выступать в качестве простой системы CI. В .git/hooks внутри любого git репозитория, вы найдете (неактивные в данный момент) файлы, которые выполняются как скрипты, когда происходит определенное действие. Напишите pre-commit хук, который запускает make paper.pdf и отклоняет коммит, если make не может быть выполнен. Этот скрипт должен предотвратить появление в любом коммите несобираемой версии paper.
  4. Настройте простую автопубликуемую страницу с помощью GitHub Pages. Добавьте GitHub Action, чтобы запустить shellcheck на любых shell-файлах в этом репозитории (вот один из способов сделать это). Проверьте, что все работает!
  5. [Создайте свой собственный] (https://help.github.com/en/actions/automating-your-workflow-with-github-actions/building-actions) GitHub Action для запуска proselint или write-good для всех файлов .md в репозитории. Включите его в своем репозитории и проверьте, что он работает, создав пул-реквест (pull request) с опечаткой в нем.

Редактировать страницу.

Лицензия CC BY-NC-SA.