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

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

Иногда дорога уже проложена для вас, так как есть очень сильный стандарт сообщества или иным образом предопределенный инструмент. Это касается:

  • Приложения Android на Java (с Ant или Maven)
  • Приложения Qt (QMake)
  • Приложения Flutter (управляемые самим Flutter)
  • Программы на Rust (со встроенным Cargo)
  • Веб-приложения Elm (скомпилированные с помощью инструмента Elm)
  • Встроенные проекты Espressif (ранее создавались с использованием настраиваемой среды Makefile, теперь переходят на CMake)
  • Встроенные проекты Microchip (с использованием их ужасного специального генератора Makefile)

Ситуация усложняется при работе в менее конкретной или более свободной среде: общая библиотека C или инструмент командной строки, встроенный проект для устройств ARM, некоторый кросс-языковой или кросс-платформенный проект, требующий настройки нескольких команд, или что-то еще, что включает нетрадиционные операции для сборки (генерация кода, синтаксический анализ пользовательской конфигурации, специальные процедуры и т. д.). Для них я часто изо всех сил пытался найти удовлетворительное решение.

Делать

Прежде чем узнавать о SCons, я часто прибегал к мастерству на все руки, которому меня всегда учили: файлам Makefile.

Make является основным элементом программного обеспечения; при загрузке исходного кода я уверен, что смогу скомпилировать его, выполнив соответствующую команду make хотя бы в 60% случаев. То, что он настолько распространен, означает, что он самодостаточен в своем распространении, несмотря на то, что на самом деле он довольно ужасен в своей работе.

В вики SCons на Github есть подробное объяснение того, почему Make не работает; Я не буду вдаваться в подробности, но, надеюсь, мы все согласимся, что это громоздкий инструмент. Что меня больше всего раздражает, так это используемый язык: очень похожий на сценарии bash (кошмар сам по себе), но также совершенно другой, достаточно, чтобы часами искать онлайн-руководства всякий раз, когда необходимо добавить новый шаг в Makefile. Сам факт, что в какой-то момент инструкции по сборке требуют отступа табуляцией вместо пробелов, для меня нарушает правила.

Затем возникает проблема работы с более крупными структурами. Make просто не масштабируется: уместить все в один сценарий нецелесообразно, а использование рекурсивных / вложенных файлов Makefiles часто приводит к ошибочным и запутанным конфигурациям.

По этим и другим причинам я начал искать естественные эволюции или альтернативы. Следующий очевидный шаг - это набор автоинструментов.

Automake и CMake

Я собираюсь объединить Automake и CMake вместе, потому что они дали мне одинаковые точные результаты; Я изучал их в указанном порядке, но никогда не связывал ни с одним из них.

Оба они представляют собой более сложные инструменты сборки, использующие язык конфигурации более высокого уровня для создания файлов сборки, которые действительно выполняют эту работу. Фактически, это их первая проблема: генерация кода.

Использование генератора кода подразумевает признание того, что базовый язык является слабым и неоптимальным, не подлежащим ремонту, как в случае с языком сценариев Makefile. Когда это происходит, я утверждаю, что должна быть очень веская причина выбрать метапрограммирование вместо простого выбора нового языка. Automake и CMake сохраняют худшее из обоих миров, определяя новый язык конфигурации (который не проще оригинального), который «компилируется» в те же старые сценарии Makefile.

Однако, когда я впервые подошел к ним, у меня не было этого предубеждения; Я был убежден, что это следующий логический шаг в моей подготовке, и решительно прыгнул в пару туториалов.

После уроков я сразу же сдался и вернулся к родному Make.

Почему так случилось? Во-первых, примеры, которые я пробовал, оставили ужасное впечатление, поскольку не существует такой вещи, как «простой пример автомата». Самый маленький привет мир требует нескольких файлов конфигурации и последовательностей команд, что значительно затрудняет процесс обучения. Я дошел до конца, не понимая ни одного шага.

Во-вторых, после полного выполнения задачи я совершенно не заметил никаких улучшений по сравнению с Make. Язык, который я использовал, был столь же сложным, процедура - такой же громоздкой, документация - столь же непонятной. Использование CMake или Automake не принесло мне никакой пользы, поэтому я просто отказался от них.

Луч надежды

Итак, я вернулся к использованию Make. Однако мне это не понравилось, поэтому я продолжал искать другой путь. В какой-то момент я даже подумал о том, чтобы просто использовать собственный сценарий bash и покончить с этим.

Наконец я наткнулся на SCons, и это была любовь с первого взгляда.

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

Фактически, SCons - это скорее библиотека Python для зависимости сборки, чем фактический инструмент сборки. Скрипты SCons - это просто скрипты Python, которые уже включают определенную среду; Честно говоря, мне бы больше понравилось, если бы потребовался явный import оператор.

SCons вызывается аналогично Make, запуском команды scons, когда файл с именем SConstruct присутствует в текущей папке.

Краткость

Используя специальные функции, разработчик определяет зависимости и команду для компиляции проекта. Самое прекрасное в этом то, что это настолько просто, насколько это необходимо. Возьмем однофайловый проект C; используя make-файл, вам нужно будет написать

hello: main.c
       gcc main.c -o hello

Обратите внимание, что это очень плохой make-файл. Я не могу избежать указания всей команды, необходимой для компиляции единого источника, мне пришлось специально использовать символ табуляции для второй строки, а хороший разработчик вместо этого использовал бы подстановочные знаки и переменные. Сам факт, что необработанная команда даст тот же результат с меньшим количеством строк кода, является хорошим индикатором того, насколько это громоздко.

На стороне SCons нет ни одного ненужного символа:

Program("hello", ["main.c"])

«Создайте программу hello из следующих источников: main.c». Ни больше ни меньше. Красивый.

Хотя это справедливо для небольших проектов, краткость - не самое заметное преимущество перед Make. Для некоторых make-файлов SCons может оказаться более подробным, поскольку имеет тенденцию к менее декларативному подходу (Python - императивный язык). Что действительно привлекает SCons, так это простота и гибкость.

Простота

Как уже говорилось, SCons - это скорее библиотека, чем специальный инструмент для сборки. В сценарии SConstruct я могу использовать язык Python во всей его красе и мощи. Скрипты SCons легко читать и создавать, потому что вам не нужно знать каждый уголок непонятного специального языка.

Помимо того, что они более простые, они имеют немедленный доступ к среде выполнения Python. Если я хочу проверить, существует ли папка в Makefile, мне нужно будет вспомнить, как это сделать в bash, спросить себя, эквивалентен ли синтаксис Make, и, наконец, выяснить, что я не могу написать команду в несколько строк по причинам. Общее время обработки: 20 минут и не менее 3 поисковых запросов в Google. Гораздо более чисто в SCons можно импортировать модуль os python и использовать os.path.exists для достижения того же результата.

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

Наконец, наличие библиотеки Python позволяет устанавливать SCons через pip. Обычно для этого нет причин, поскольку в большинстве операционных систем есть предварительно созданные двоичные версии каждого пакета Python. Если вам не хватает роскоши быть sudoer, вы все равно можете воспользоваться scons, установив его как пользователь с pip --user или virtualenv.

Масштабируемый

Несмотря на свою более простую природу, SCons аккуратно адаптируется к более крупным сборкам. Во-первых, гораздо проще добавлять новые исходные файлы и ветки благодаря немедленной функции Glob для сопоставления с образцом. Вместо создания иерархических сценариев или кропотливого сопоставления каждого исходного файла каждая новая папка может быть объединена в один вызов функции:

sources = Glob("{}/*.S".format(BUILD))
sources += Glob("{}/*.c".format(BUILD))
sources += Glob("{}/emulated/*.c".format(BUILD))
sources += Glob("{}/hal/*.c".format(BUILD))
sources += ["res/font.bin"]
env = Environment(**env_options) # My environment options
env.Program(ALL, sources)

Здесь я включаю исходники ассемблера, файлы C из трех разных каталогов и двоичный шрифт. Затем директива Program просто дает команду использовать их все для создания конечного продукта, и каждый файл будет обрабатываться соответствующим образом: исходные тексты ассемблера будут собраны, файлы C будут скомпилированы, а двоичный шрифт font.bin, являющийся уже обработанным результатом, будет использоваться только на заключительном этапе связывания.

Также обратите внимание, насколько просто включить новые файлы, используя регулярные выражения через функцию Glob(), в отличие от странных макросов подстановки и подстановочных знаков Makefile.

Хотя я обычно не рекомендую их использовать, иерархические сборки все же возможны. Используя функцию SConscript, можно вызывать отдельные сценарии для структурированных проектов; в отличие от make, процесс для передачи окружения и параметров намного чище. С помощью make-файлов каждая сущность среды оболочки становится переменной, доступной из сценария; при вызове рекурсивной программы make передаются переменные из среды и командной строки, а определенные пользователем игнорируются (если не указано в директиве export). На практике это приводит к очень грязной и грязной глобальной окружающей среде.

В SCons среды - это словари, которые передаются по мере необходимости различными сценариями. Переход от глобальной переменной к постоянно присутствующему параметру сначала может показаться неважным, но в конечном итоге это действительно изменение качества жизни.

Несколько примеров

В завершение я приведу пару практических примеров светильников, которые я использую сейчас.

Первый используется для компиляции прошивки микроконтроллера stm32 ARM. Поскольку архитектура отличается (если вы не работаете на Raspberry Pi), сценарии должны указывать кросс-компилятор и сценарий компоновщика; Кроме того, я работаю с libopencm3, поэтому есть статический архив, который нужно связать с моим исходным кодом. Это началось как Makefile, но результат быстро становился беспорядочным.

import os
TOOLCHAIN = "arm-none-eabi-"
ELF = "experiment.elf"
BIN = "experiment.bin"
LIBOPENCM3 = "/home/maldus/Source/Github/libopencm3/"
CPUFLAGS = ["-mcpu=cortex-m3", "-mthumb"]
CFLAGS = ["-Wall", "-Wextra", "-g3", "-O0", "-MD",
          "-DSTM32F1", "-I{}include".format(LIBOPENCM3)] + CPUFLAGS
LDFLAGS = ["-nostartfiles", "-L{}lib".format(
    LIBOPENCM3), "-Wl,-T,{}lib/stm32/f1/stm32f103x8.ld".format(LIBOPENCM3)] + CPUFLAGS
LDLIBS = ["-lopencm3_stm32f1"]
# Creates a Phony target
def PhonyTargets(
        target,
        action,
        depends,
        env=None,
):
    if not env: env = DefaultEnvironment()
    t = env.Alias(target, depends, action)
    env.AlwaysBuild(t)
externalEnvironment = {}
if 'PATH' in os.environ.keys():
    externalEnvironment['PATH'] = os.environ['PATH']
env_options = {
    "ENV": externalEnvironment,
    "CC": "{}gcc".format(TOOLCHAIN),
    "CPPPATH": ["{}/include".format(LIBOPENCM3)],
    "CCFLAGS": CFLAGS,
    "LINKFLAGS": LDFLAGS,
    "LIBS" : LDLIBS
}
sources = Glob("*.c")
env = Environment(**env_options)
env.Program(ELF, sources)
env.Command(BIN, ELF, "{}objcopy -O binary {} {}".format(TOOLCHAIN, ELF, BIN))
env.Default(BIN)
PhonyTargets("flash", "st-flash write {} 0x8000000".format(BIN), BIN, env)
PhonyTargets("openocd", "openocd -f interface/stlink-v2.cfg -f target/stm32f1x.cfg", BIN, env)

В начале скрипта я определяю библиотеки, утилиты и путь к инструментальной цепочке, которые будут использоваться. Затем они форматируются в словарь env_options, эффективно объявляя желаемое поведение для процесса сборки.

После того, как все настроено, компиляция запрашивается всего двумя строками кода:

env.Program(ELF, sources)
env.Command(BIN, ELF, "{}objcopy -O binary {}

Создайте ELF из исходников, затем отформатируйте его в двоичный файл. Остальная часть скрипта определяет пару фальшивых целей, которые на самом деле не производят выходных данных (аналогично директиве make .phony), например, flash для программирования устройства и openocd для его отладки. Для выполнения двоичных файлов, таких как st-flash и openocd, внешняя среда должна быть извлечена и добавлена ​​в env_options (то же самое касается любого графического интерфейса пользователя, поскольку переменная DISPLAY может отсутствовать в среде scons).

Второй пример - полная противоположность: веб-сайт, созданный с помощью Elm. Elm - это полнофункциональный язык программирования, скомпилированный в Javascript для создания веб-приложений; он поставляется через npm с собственным инструментом сборки (elm CLI), так в чем польза SCons?

На самом деле немного, но дьявол кроется в деталях. Веб-сайт, который я создаю, использует Bootstrap, который, в свою очередь, требует конфигурации на основе sass. Sass - это эволюция языков стилей, о которых я не знал, более чистый и высокоуровневый язык, который все еще компилируется в css. Итак, мой проект теперь имеет двухэтапный процесс компиляции, три, если вы считаете вызов elm reactor для запуска локального веб-сервера и просмотра изменений. Посмотрим, как SCons с этим справится.

import os
ELM = "elm.js"
CSS = "compiled.css"
# Creates a Phony target
def PhonyTargets(
        target,
        action,
        depends,
        env=None,
):
    if not env: env = DefaultEnvironment()
    t = env.Alias(target, depends, action)
    env.AlwaysBuild(t)
externalEnvironment = {}
if 'PATH' in os.environ.keys():
    externalEnvironment['PATH'] = os.environ['PATH']
env_options = { "ENV": externalEnvironment }
env = Environment(**env_options)
final = env.Alias("all", [ELM, CSS])
env.Default(final)
env.Command(ELM, "src/main.elm", "elm make src/main.elm --output={}".format(ELM))
env.Command(CSS, "custom.scss", "sass custom.scss > {}".format(CSS))
PhonyTargets("reactor", "elm reactor", final, env)

Учитывая конечную цель, это может показаться излишним, но синтаксис Python настолько прост и мнемоничен, что с моей стороны для этого почти не потребовалось никаких усилий. Неудивительно, что ни elm, ни sass изначально не поддерживаются SCons, но это не проблема: функция Command служит именно цели указания любой пользовательской команды для генерации вывода.

Теперь, запустив scons reactor, сценарий обновляет файлы css и js перед запуском веб-сервера разработки, экономя драгоценные секунды, когда перестройка конфигурации sass не требуется.

Итог: Make был отличным, теперь давайте продолжим. Генератор make-файлов - это способ исправить проблему некрасивым патчем, но сегодня у нас есть средства для правильного решения. SCons прост, мощный и эффективный, и я должен был услышать о нем гораздо раньше; если вам это нужно, попробуйте!