Прощай, cabal. Здравствуй, stack!

Приветствую, друзья!

Наконец-то я добрался до этой темы. А то слух, знаете ли, всё громче и громче, а что к чему - знают не все. В общем, решил я разобраться с новым Haskell-инструментом под названием stack.

О чём идёт речь

Утилита stack - это новый инструмент от команды FP Complete, предназначенный для работы с Haskell-проектами. Штуковина очень молодая (первая версия датируется июнем сего года), однако шуму наделать уже успела.

Постойте, скажете вы, у нас же есть старый добрый cabal! Да, есть, но, как показала практика, не такой уж он и добрый. И вот теперь настал его смертный час, ибо к нам пришёл stack

Замена Cabal?

Определимся с понятиями. Когда мы слышим слово “кабал” - речь может идти о:

  1. спецификации Cabal (“Common Architecture for Building Applications and Libraries”) и формате метаданных Haskell-проекта. Тот самый формат, который мы видим в файле с расширением .cabal;
  2. реализации спецификации Cabal в виде библиотеки Cabal;
  3. утилите cabal-install, в виде знакомой нам команды cabal.

Так вот утилита stack - это замена утилите cabal-install. Больше не будет никаких cabal update и cabal sandbox init. Кстати, некоторые ошибочно полагали, что stack представляет собой надстройку над cabal-install и скрыто использует последнюю, однако это не так: stack использует Cabal-библиотеку (см. выше пункт 2), но никак не соприкасается с cabal-install. Иными словами, вы можете спокойно удалить cabal-install с вашего компьютера и забыть о ней навсегда.

Зачем это было нужно

По результатам большого опроса, проведённого среди множества коммерческих пользователей Haskell, главной болью при работе с Haskell-проектами была-таки cabal-install. Для этой боли даже название придумали: “cabal hell”. А всё дело было в сложных/перекрёстных зависимостях между (многочисленными) пакетами, задействованными в ваших проектах. Фундаментальная проблема cabal-install такова:

То, что работает сегодня, может перестать работать завтра.

Естественно, такое положение дел не устраивало сообщество, а в особенности тех, кто уже использовал Haskell в коммерческих проектах. Никому не хочется столкнуться с ситуацией, когда, после очередного cabal update, проект, прекрасно собирающийся вчера вечером, перестаёт собираться сегодня днём. Подобная непредсказуемость неизбежно разрушает доверие.

Были предприняты попытки решить указанную проблему. В cabal-1.18 были введены песочницы, а в cabal-1.20 - команда cabal freeze. Теперь проекты могли быть изолированными друг от друга, а пакетные зависимости можно было заморозить. И проблемы действительно уменьшились, но, к сожалению, не ушли. Поэтому команда FP Complete создаёт stack. Цель этого проекта - избавить мир от cabal hell, раз и навсегда. А заодно максимально упростить работу с проектами.

Поехали

Идём сюда и скачиваем stack для своей ОС. На момент написания данной статьи последней версией была 1.2.0, датируемая 6 июля. После скачивания архива распаковываем его - и вот он наш файлик. Переименовываем его в stack, делаем исполняемым, копируем куда-нибудь в PATH (я положил в /usr/local/bin) - и можно работать.

Начнём с пустышки

Делаем следующее:

Команда new создаёт простейший проект-пустышку в текущем каталоге:

Перед нами - самый обыкновенный Haskell-проект. Кстати, он прямо сейчас, без каких-либо дополнительных манипуляций готов к сборке, в отличие от проекта, создаваемого с помощью cabal init.

Самая важная часть, которой не было раньше - файл stack.yaml. Заглянем в него:

Обратите внимание на последнюю строчку, ибо она очень, очень важна. lts-2.15 - это версия нашей Haskell-экосистемы. LTS - от “Long Term Support” - представляет собой фиксированный набор Haskell-пакетов. Но что значит “фиксированный набор”? Поясню.

Stackage

Как мы знаем, наш любимый Hackage содержит очень много пакетов: на данный момент их чуть более 8400 штук. Но будет честными - далеко не всё из этой кучи пригодно для реального применения. Многие из пакетов - сугубо экспериментальные (читай “игрушечные”), а есть и вообще заброшенные. Поэтому друзья из FP Complete придумали Stackage, от “Stable Hackage”.

Идея в следующем: отобрать часть конкретных пакетов - и закрепить их в своего рода снимке (snapshot), присвоив этому снимку версию. Например, снимок версии 2.15 включает в себя 1066 пакетов, причём версия каждого из них явно указана (взгляните на список).

Именно благодаря этому мы избавляемся от упомянутой выше непредсказуемости. Версия lts-снимка однозначно определяет версию каждого пакета, используемого в нашем проекте. Поэтому когда N программистов, работающих с неким проектом, используют одну и ту же версию lts-снимка - у них всё гарантированно работает (как сегодня, так и завтра).

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

Собираем

Пора собрать наш проект:

В начале происходит загрузка “lts-2.15 build plan”, чтобы stack понял, с какими версиями пакетов следует работать в данном проекте. А теперь обратите внимание на последнюю строчку: stack сообщает нам о несоответствии версий GHC. На моей машине стоит 7.8.3, однако ожидается версия 7.8.4. Заглянем на lts-страницу - так и есть:

LTS Haskell 2.15 - GHC 7.8.4

Данный снимок был проверен с использованием 7.8.4, а следовательно, эту версию и нужно использовать. Однако на моей машине, в рамках последней Haskell Platform, установлена версия 7.8.3. Что же делать? Следует установить версию 7.8.4 (спасибо, Кэп!).

Ставим нужный компилятор

К счастью, возможность установки нужного компилятора входит в обязанности stack! Да-да, он и это умеет. Более того, он сам подсказал нам нужную команду. Вспомните ту самую последнюю строчку, в самом конце: “Try running stack setup”. Сделаем же это:

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

Новая версия 7.8.4 будет использоваться исключительно в процессе сборки нашего проекта. А кстати, вдруг я вас обманываю? Как мы можем проверить версию реально используемого компилятора? Очень просто, ведь stack умеет запускать интерпретатор ghci:

Та-дам! Действительно 7.8.4.

Теперь уже точно собираем

Выполняем:

Как видите, в корне проекта появился скрытый каталог .stack-work, в котором и осуществляется вся сборка. Взгляните, куда установилась наша программка:

Как видите, указана не только версия используемого компилятора, но и версия используемого lts-снимка.

Проверяем версии пакетов

Итак, собрали. А теперь убедимся в правоте наших ожиданий в отношении версий используемых пакетов.

Раньше версии пакетов (или диапазоны версий) указывались в .cabal-файле, в секции build-depends:

Теперь же, как мы помним, версии пакетов определяются используемым нами lts-снимком. Проверим это.

Я воспользуюсь одним из своих блогов, создаваемых, как многие знают, с помощью hakyll. Проект зависит от маленькой библиотеки split, помогающей разделывать строчки на кусочки. lts-снимок версии 2.15 содержит указание версии:

Итак, используется 0.2.2. А теперь я сделаю вид, что напутал с версиями и указал в .cabal-файле следующее:

На самом деле, версия 0.2.2 является последней, но суть не в этом. Суть в том, что проект не соберётся, потому что stack строг и аккуратен по отношению к версиям.

Первое, на что следует обратить внимание - новая команда. Мы используем stack init, потому что собираемся использовать stack для уже существующего Haskell-проекта. stack создаёт в его корне уже знакомый нам файл stack.yaml. И кстати, если в корне вашего проекта есть cabal-песочницы или файл cabal.config - смело удаляйте всё это.

Сразу после создания .yaml-файла stack проверяет готовность проекта быть собранным в соответствии с lts-снимком версии 2.15. И тут - неприятный, но вполне ожидаемый результат:

Версии пакета в .lts-снимке и в .cabal-файле различаются, поэтому сборка не пройдёт. Решение - убрать указание версий из .cabal-файла (ещё раз спасибо, Кэп!). Больше никакой путаницы - версии всех используемых пакетов явно указаны в lts-снимке, и нигде более.

Больше компиляторов, хороших и разных

Ну что ж, раз можно поставить дополнительный “локальный” компилятор, исходя из версии экосистемы конкретного проекта, значит, можно поставить и несколько компиляторов.

Допустим, я хочу поэкспериментировать с новым GHC 7.10.1 с существующим проектом. Перехожу в проект:

Теперь открываем stack.yaml и заменяем последнюю строчку на:

Оказывается, lts-снимок - не единственная возможность указания Haskell-экосистемы. LTS прекрасно подходит для серьёзных долгоживущих проектов, но для экспериментаторов, предпочитающих быть на гребне волны, подойдёт одна из ночных сборок. В данном случае - сборка от 12 июля сего года. Живёт она здесь. Откроем её:

Stackage Nightly 2015-07-12 - GHC 7.10.1

То, что нам нужно! Раз данная сборка проверена с помощью 7.10.1, то при выполнении команды stack setup произойдёт установка именно этой версии компилятора:

И теперь у нас есть два разных компилятора, не мешающих друг другу. Соответственно, все мои проекты, зависящие от 7.8.4, будут использовать только его, а проекты, зависящие от 7.10.1 - его. Никакой путаницы.

А куда он ставит пакеты?

До появляения cabal sandbox все пакеты ставились в глобальное пространство, и это было ужасно. Идея песочницы частично решила проблему: каждый проект теперь скрыто включал свою собственную песочницу, в которую устанавливалось всё, необходимое для данного проекта.

Вроде бы неплохое решение, но оно грубое в силу своей избыточности. Представьте себе, что у вас на машине есть три yesod-проекта. Соответственно, у вас появится три песочницы с тремя полными копиями всех пакетов, входящих в yesod-зависимости (кто работал с yesod, тот знает, что зависимостей этих, мягко выражаясь, много). Во-первых, попусту растрачивается дисковое пространство, а во-вторых, при появлении четвёртого yesod-проекта (ну вот любите вы yesod, почему бы и нет?) весь yesod будет заново установлен в чертвёртую песочницу. И особенно обидно будет при условии, если во всех четырёх проектах используется одна и та же версия yesod

В stack всё умнее. Больше никаких избыточных копий пакетов внутри каждого из проектов. Теперь пакеты устанавливаются в единое глобальное место, а именно в упомянутый ранее скрытый каталог .stack в вашем домашнем каталоге. Но! Пакеты больше не валятся в одну кучу. Напротив, они устанавливаются иерархично, в соответствии с lts-снимками (ну и ночными сборками, если таковые используются).

Заглянем:

Вот они, наши снимки. Глянем поглубже:

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

Таким образом, если упомянутые ранее три yesod-проекта используют одну и ту же версию lts-снимка - скажем, 2.15 - тогда использованы будут пакеты из каталога ~/.stack/snapshots/x86_64-osx/lts-2.15/7.8.4/. Если же четвёртый yesod-проект будет использовать, скажем, одну из ночных сборок совместно с GHC 7.10.1, то будут использованы пакеты из ~/.stack/snapshots/x86_64-osx/nightly-2015-07-12/7.10.1/. Никаких излишних копий, и при этом никаких конфликтов.

За пределами снимка

Как было сказано выше, lts-снимки (равно как и ночные сборки) содержат не всё то, что есть на просторах Hackage. Ну а вдруг мой проект использует некий пакет, которого нет в используемом мною снимке! Что делать?

Допустим, мне понадобился какой-нибудь string-qq. Если я просто укажу его в своём .cabal-файле, при попытке сборки stack выразит свой категорический протест:

Поэтому открываем stack.yaml, ищем строчку:

и заменяем её на:

Готово. Теперь при сборке stack увидит, что данный пакет нужно искать не в используемом снимке, а напрямую в Hackage. Соответственно, если мне вдруг понадобится более одного внешнего пакета, я просто дописываю их следом:

И всё заработает.

Установка в удобное место

Иногда после сборки проекта бывает удобно установить исполняемый файл приложения в какое-нибудь доступное место. Для этого служит команда install:

И если путь ~/.local/bin/ добавлен в PATH, то приложение будет немедленно доступно напрямую. Весьма удобно.

Вывод

Вывод только один - классную штуку сделали друзья из FP Complete. Лично я перевожу на stack все мои проекты. Прощай, cabal!

Кстати, если вы работаете на Linux - для вас хорошая новость: для Ubuntu, Debian, CentOS/Red Hat и Fedora уже готовы соответствующие пакеты. Ну а нам, маководам, осталось дождаться добавления stack в brew… ;-)

Полезные ссылки