Делай как вGoogle
Скачать 5.77 Mb.
|
373 сборки в других каталогах, связывать их друг с другом и добавлять новые задачи, зависящие от имеющихся. Нам достаточно только передать имя одной задачи ин- струменту командной строки ant , и он позаботится обо всем остальном. Ant — это очень старая система. Первая ее версия была выпущенная в 2000 году и была совсем не похожа на «современную» систему сборки! За прошедшие годы появились другие системы, такие как Maven и Gradle, по сути заменившие Ant. Они добавили такие возможности, как автоматическое управление внешними зависимо- стями и более ясный синтаксис без XML. Но природа этих новых систем осталась прежней: они позволяют инженерам писать сценарии сборки принципиальным и модульным способом в виде перечней задач и предоставляют инструменты для их выполнения и управления зависимостями. Темная сторона систем сборки, основанных на задачах По сути, эти системы позволяют инженерам определить любой сценарий как задачу и реализовать практически все, что только можно вообразить. Но работать с ними становится все труднее с увеличением сложности сценариев сборки. Проблема таких систем состоит в том, что они многое перекладывают на плечи инженеров, почти ни- чего не делая самостоятельно. Система не знает, что делают сценарии, и из-за этого страдает производительность, потому что система вынуждена действовать макси- мально консервативно, планируя и выполняя этапы сборки. Кроме того, система не может убедиться, что каждый сценарий выполняет то, что должен, поэтому сценарии усложняются и требуют отладки. Сложность параллельного выполнения этапов сборки. Современные рабочие станции, используемые для разработки, обычно обладают большой вычислительной мощностью, имеют процессоры с несколькими ядрами и теоретически способны вы- полнять несколько этапов сборки параллельно. Но системы на основе задач часто не могут выполнять задачи параллельно, даже если это выглядит возможным. Пред- положим, что задача A зависит от задач B и C. Поскольку задачи B и C не зависят друг от друга, безопасно ли выполнять их одновременно, чтобы система могла бы- стрее перейти к задаче A? Возможно, если они не используют одни и те же ресурсы. Но если они используют один и тот же файл для хранения статуса выполнения, то их одновременный запуск вызовет конфликт. В общем случае система ничего не знает об этом, поэтому она должна рисковать вероятностью конфликтов (которые могут приводить к редким, но очень трудным для отладки проблемам сборки) или ограничиться выполнением сборки в одном потоке и в одном процессе. Из-за этого огромная вычислительная мощь машины разработчика может недоиспользоваться, возможность распределения сборки между несколькими машинами будет полностью исключена. Сложности инкрементального выполнения сборки. Хорошая система сборки позволяет инженерам выполнять инкрементальные сборки, когда небольшое изменение не требует повторной сборки всей кодовой базы с нуля. Это особенно важно, если система сборки работает медленно и не может выполнять этапы сборки 374 Глава 18. Системы и философия сборки параллельно по вышеупомянутым причинам. Но, к сожалению, и здесь системы сборки, основанные на задачах, показывают себя не с лучшей стороны. Поскольку задачи могут делать что угодно, невозможно проверить, были ли они уже выполнены. Многие задачи просто берут набор исходных файлов и запускают компилятор, чтобы создать набор двоичных файлов, поэтому их не нужно запускать повторно, если ис- ходные файлы не изменились. Но, не имея дополнительной информации, система не может знать, загружает ли задача файл, который был изменен, или записывает ли она отметку времени, изменяющуюся при каждом запуске. Чтобы гарантировать безошибочность, система часто вынуждена повторно запускать каждую задачу во время каждой сборки. Некоторые системы сборки пытаются разрешить инкрементальные сборки, по- зволяя инженерам указывать условия, при которых задача должна быть повторно запущена. Эта возможность реализуется гораздо сложнее, чем кажется. Например, в таких языках, как C++, которые позволяют напрямую подключать другие файлы, невозможно определить весь набор файлов, за изменениями в которых необходимо следить, без анализа исходного кода. Инженеры строят догадки и нередко ошибочно используют результат задачи повторно. Когда такое случается слишком часто, у ин- женеров вырабатывается привычка запускать задачу чистки ( clean ) перед каждой сборкой, чтобы обновить состояние, что полностью лишает смысла инкрементальную сборку. На удивление сложно выяснить, когда задача должна выполняться повторно, и с этим машины справляются лучше, чем люди. Сложности сопровождения и отладки сценариев. Наконец, сами сценарии сбор- ки, используемые системами сборки на основе задач, часто слишком сложны для сопровождения. Им часто уделяется меньше внимания, но, тем не менее, сценарии сборки — это точно такой же код, как и собираемая система, и в них тоже могут по- являться ошибки. Вот несколько примеров ошибок, которые очень часто допускаются при работе с системой сборки на основе задач: y Задача A зависит от задачи B, ожидая получить от нее определенный файл. Вла- делец задачи B не понимает, что от нее зависят другие задачи, поэтому он меняет ее, и в результате файл, генерируемый задачей B, оказывается в другом месте. Эта ошибка никак не проявляет себя, пока кто-то не попытается запустить задачу A и не обнаружит, что она терпит неудачу. y Задача A зависит от задачи B, которая зависит от задачи C, которая создает определенный файл, необходимый задаче A. Владелец задачи B решает, что она больше не должна зависеть от задачи C, что заставляет задачу A терпеть неудачу, потому что задача B больше не вызывает задачу C! y Разработчик новой задачи делает ошибочное предположение о машине, на которой выполняется задача, например о местоположении инструмента или значениях определенных переменных окружения. Задача работает на его компьютере, но терпит неудачу на компьютере другого разработчика. Современные системы сборки 375 y Задача выполняет недетерминированную операцию, например загружает файл из интернета или добавляет отметку времени в сборку. Из-за этого разработчики получают потенциально разные результаты при каждом запуске сборки, а значит, не всегда могут воспроизвести и исправить ошибки, возникающие друг у друга или в автоматизированной системе сборки. y Задачи с множественными зависимостями могут оказываться в состоянии гон- ки. Если задача A зависит от задач B и C, а задачи B и C изменяют один и тот же файл, то задача A будет получать разный результат, в зависимости от того, какая из задач — B или C — завершится первой. Нет универсального рецепта решения этих проблем производительности, безошибоч- ности или удобства сопровождения в рамках описанной здесь структуры задач. Пока инженеры пишут произвольный код для выполнения во время сборки, у системы не будет достаточно информации, чтобы всегда быстро и безошибочно производить сборку. Чтобы решить эту проблему, нужно отобрать часть полномочий у инженеров и передать их системе, а также переосмыслить роль системы, но уже не в терминах выполняемых задач, а в терминах создаваемых артефактов. Этот подход используется в Blaze, и Bazel и описывается в следующем разделе. Системы сборки на основе артефактов Чтобы создать более удачную систему сборки, нужно отступить на шаг назад. Про- блема ранних систем состоит в том, что они позволяли инженерам определять свои задачи. Возможно, лучше предложить инженерам небольшое количество задач, определяемых системой, которые они смогут настраивать в ограниченных преде- лах. И самой главной задачей выступит сборка кода. Инженеры по-прежнему будут сообщать системе, что собирать, но как выполнять сборку, будет решать система. Именно этот подход использован в Blaze и в других системах сборки на основе ар- тефактов, которые произошли от нее (включая Bazel, Pants и Buck). В них тоже есть файлы сборки, но вместо набора императивных команд на языке сценариев, полного по Тьюрингу, описывающих порядок создания выходных данных, они де- кларативно описывают набор артефактов, который нужно получить, их зависимости и ограниченный набор параметров, влияющих на процесс сборки. Когда инженеры выполняют команду blaze в командной строке, они сообщают ей набор целей для сборки («что»), а Blaze отвечает за настройку, выполнение и планирование этапов компиляции («как»). Поскольку такая система сборки полностью контролирует вы- бор и очередность выполнения инструментов, она имеет более надежные гарантии, которые позволяют ей действовать эффективно и без ошибок. Функциональная перспектива Легко провести аналогию между системами сборки на основе артефактов и функ- циональным программированием. Программы на традиционных императивных языках (таких как Java, C и Python) определяют списки операторов, которые должны 376 Глава 18. Системы и философия сборки выполняться в определенной очередности, точно так же, как системы сборки на ос- нове задач позволяют программистам определять последовательность выполняемых этапов. Программы на языках функционального программирования (например, Haskell и ML), напротив, имеют структуру, больше похожую на серию математиче- ских уравнений. В них программист описывает вычисления, но выбор, когда и как их выполнить, он оставляет компилятору. Это соответствует идее декларативного описания задач, получив которое система сборки на основе артефактов сама выби- рает, как их выполнить. Многие задачи сложно выразить на языке функционального программирования, но когда такое возможно, задачи получают дополнительную выгоду: компилятор с лег- костью может организовать их параллельное выполнение и дать строгие гарантии их безошибочности, что невозможно в императивных языках. Функциональные языки позволяют выражать задачи, связанные с простым преобразованием одного фрагмента данных в другой, в виде набора правил или функций. И это именно то, что нужно системе сборки: вся система фактически является математической функ- цией, которая принимает исходные файлы (и инструменты, такие как компилятор) на входе и возвращает двоичные файлы на выходе. Конкретный пример в Bazel. Bazel — это открытая версия внутреннего инструмента сборки Blaze, используемой в Google, и хороший пример системы сборки на основе артефактов. Вот как выглядит файл сборки в Bazel (которому обычно дается имя BUILD ): java_binary( name = "MyBinary", srcs = ["MyBinary.java"], deps = [ ":mylib", ], ) java_library( name = "mylib", srcs = ["MyLibrary.java", "MyHelper.java"], visibility = ["//java/com/example/myproduct:__subpackages__"], deps = [ "//java/com/example/common", "//java/com/example/myproduct/otherlib", "@com_google_common_guava_guava//jar", ], ) В системе Bazel файлы BUILD определяют цели. В этом примере определены два типа целей: java_binary и java_library . Каждая цель соответствует артефакту, который может быть создан системой: цели binary создают двоичные файлы, выполняемые непосредственно, а цели library создают библиотеки, которые могут использовать- Современные системы сборки 377 ся двоичными файлами или другими библиотеками. Каждая цель имеет атрибуты name (определяет имя для ссылки в командной строке и в других целях), srcs (опре- деляет список файлов с исходным кодом, которые необходимо скомпилировать для создания артефакта цели) и deps (определяет другие цели, которые должны быть собраны перед этой целью и связаны с ней). Зависимости ( deps ) могут нахо- диться в одном пакете с основной целью (как, например, зависимость ":mylib" для MyBinary ), в разных пакетах, но в одной иерархии исходного кода (как, например, зависимость "//java/com/example/common" для mylib ) или в сторонних артефактах, находящихся за пределами иерархии исходного кода (как, например, зависимость "@com_google_common_guava_guava//jar" для mylib ). Иерархии исходного кода на- зываются рабочими пространствами и определяются наличием специального файла WORKSPACE в корневом каталоге. Сборка в Bazel запускается с помощью инструмента командной строки, так же как при использовании Ant. Чтобы собрать цель MyBinary , пользователь должен вы- полнить команду bazel build :MyBinary . При первом вызове этой команды в чистом репозитории Bazel выполнит следующие действия: 1. Проанализирует все файлы BUILD в рабочем пространстве и создаст граф зави- симостей между артефактами. 2. Использует граф, чтобы определить транзитивные (промежуточные) зависимо- сти для MyBinary ; то есть все цели, от которых зависит MyBinary , а также все цели, от которых зависят эти цели, и т. д. 3. По порядку соберет каждую из этих зависимостей (или загрузит, если зависимо- сти внешние). Bazel начинает сборку с целей, не имеющих других зависимостей, и проверяет, какие еще зависимости необходимо собрать для каждой цели. После сборки зависимостей Bazel приступает к сборке самой цели. Этот процесс про- должается, пока не будут собраны все транзитивные зависимости для MyBinary 4. Соберет MyBinary и создаст окончательный выполняемый двоичный файл, вклю- чающий все зависимости, собранные на шаге 3. Может показаться, что этот процесс не сильно отличается от процесса в системе сборки, основанной на задачах. И действительно, конечный результат — все тот же двоичный файл, и процесс его создания основан на определении последователь- ности этапов для сборки зависимостей и последующего их выполнения по порядку. Но в этих подходах есть принципиальные отличия. Первое отличие — в шаге 3 Bazel знает, что в результате сборки каждой цели будут созданы библиотеки Java и для этого требуется запустить компилятор Java, а не произвольный пользовательский сценарий, поэтому она без опаски выполняет эти шаги параллельно. В результате производительность может увеличиться на порядок по сравнению с последователь- ной сборкой целей на машине с одноядерным процессором. Это возможно только потому, что подход на основе артефактов возлагает всю ответственность за выбор политики действий на систему сборки, чтобы та могла получить более надежные гарантии относительно параллелизма. 378 Глава 18. Системы и философия сборки Однако параллелизм — не единственное преимущество. Другое преимущество, кото- рое дает второй подход, становится очевидным, когда разработчик вводит команду bazel build :MyBinary во второй раз, не внося в исходный код никаких изменений. Bazel завершает работу менее чем через секунду и сообщает, что цель обновлена. Это возможно благодаря парадигме функционального программирования — Bazel знает, что каждая цель является результатом запуска компилятора Java. Если входные дан- ные не менялись, можно повторно использовать результат компилятора, полученный ранее, и это верно на всех уровнях. Если MyBinary.java изменится, Bazel поймет, что нужно повторно собрать MyBinary и использовать при этом ранее собранную цель mylib . Если изменится исходный код //java/com/example/common , то Bazel поймет, что нужно повторно собрать эту библиотеку, а также цели mylib и MyBinary и ис- пользовать при этом ранее собранную библиотеку //java/com/example/myproduct/ otherlib . Поскольку системе Bazel известны свойства инструментов, используемые на каждом этапе сборки, она может каждый раз повторно собирать минимальный набор артефактов, гарантируя, что цели, исходный код которых не изменялся, по- вторно собираться не будут. Переосмысление процесса сборки в терминах артефактов, а не задач — дело тонкое, но стоящее. Ограничивая возможности программиста, система сборки может получить более точное представление о том, что делается на каждом этапе сборки, и использо- вать эти знания, чтобы повысить эффективность сборки за счет параллельного вы- полнения этапов и повторного использования результатов. Но на самом деле это лишь первый шаг на пути к распределенной и хорошо масштабируемой системе сборки. Другие небольшие хитрости Bazel Системы сборки на основе артефактов решают проблемы параллелизма и повторного использования, присущие системам сборки на основе задач. Но есть еще несколько проблем, которые мы не осветили. В Bazel были найдены для них интересные реше- ния, и мы должны обсудить их, прежде чем двигаться дальше. Инструменты как зависимости. Поскольку результаты сборки зависят от инстру- ментов, установленных на компьютерах, в разных системах из-за использования разных версий инструментов или их установки в разных местах, не всегда можно воспроизвести одни и те же результаты. Проблема становится еще более сложной, когда в проекте используются языки, требующие разных инструментов, собранных или скомпилированных на разных платформах (например, в Windows или Linux), и для разных платформ требуются разные наборы инструментов для выполнения одной и той же работы. Bazel решает первую часть этой проблемы, интерпретируя инструменты как зависи- мости для каждой цели. Каждая библиотека java_library в рабочем пространстве неявно зависит от хорошо известного компилятора Java, который может быть на- строен глобально на уровне рабочего пространства. Когда Blaze выполняет сборку java_library , она проверяет доступность указанного компилятора в известном месте и загружает его, если проверка дала отрицательный результат. Компилятор Java Современные системы сборки 379 интерпретируется как зависимость: если он изменится, система повторно соберет все артефакты, зависящие от него. Для всех типов целей в Bazel используется одна и та же стратегия объявления инструментов, которые должны запускаться, и это гарантирует, что Bazel сможет загрузить их, в какой бы системе она ни работала. Вторая часть проблемы — независимость от платформы — в Bazel решается с помо- щью наборов инструментов ( https://oreil.ly/ldiv8 ). Вместо зависимости от конкретных инструментов цели фактически зависят от типов наборов инструментов. Набор ин- струментов содержит инструменты и обладает некоторыми свойствами, определяю- щими, как цель данного типа должна собираться на конкретной платформе. Рабочее пространство может определять конкретный набор инструментов для данного типа в зависимости от данного сетевого узла и целевой платформы (подробнее об этом в руководстве по Bazel). |