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

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

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

Для большинства проектов, содержат они код или нет (например, 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. Все они работают по похожему сценарию: при любом изменении кода запускаются тесты.

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

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

Exercises

  1. Most makefiles provide a target called clean. This isn’t intended to produce a file called clean, but instead to clean up any files that can be re-built by make. Think of it as a way to “undo” all of the build steps. Implement a clean target for the paper.pdf Makefile above. You will have to make the target phony. You may find the git ls-files subcommand useful. A number of other very common make targets are listed here.
  2. Take a look at the various ways to specify version requirements for dependencies in Rust’s build system. Most package repositories support similar syntax. For each one (caret, tilde, wildcard, comparison, and multiple), try to come up with a use-case in which that particular kind of requirement makes sense.
  3. Git can act as a simple CI system all by itself. In .git/hooks inside any git repository, you will find (currently inactive) files that are run as scripts when a particular action happens. Write a pre-commit hook that runs make paper.pdf and refuses the commit if the make command fails. This should prevent any commit from having an unbuildable version of the paper.
  4. Set up a simple auto-published page using GitHub Pages. Add a GitHub Action to the repository to run shellcheck on any shell files in that repository (here is one way to do it). Check that it works!
  5. Build your own GitHub action to run proselint or write-good on all the .md files in the repository. Enable it in your repository, and check that it works by filing a pull request with a typo in it.

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

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