Прощай, cabal. Здравствуй, stack!
date = fromGregorian 2015 jul 13
category = "Утилиты"
tags = ["stack", "cabal"]
Приветствую, друзья!
Наконец-то я добрался до этой темы. А то слух, знаете ли, всё громче и громче, а что к чему - знают не все. В общем, решил я разобраться с новым Haskell-инструментом под названием stack
.
О чём идёт речь
Утилита stack
- это новый инструмент от команды FP Complete, предназначенный для работы с Haskell-проектами. Штуковина очень молодая (первая версия датируется июнем сего года), однако шуму наделать уже успела.
Постойте, скажете вы, у нас же есть старый добрый cabal
! Да, есть, но, как показала практика, не такой уж он и добрый. И вот теперь настал его смертный час, ибо к нам пришёл stack
…
Замена Cabal?
Определимся с понятиями. Когда мы слышим слово “кабал” - речь может идти о:
- спецификации Cabal (“Common Architecture for Building Applications and Libraries”) и формате метаданных Haskell-проекта. Тот самый формат, который мы видим в файле с расширением
.cabal
; - реализации спецификации Cabal в виде библиотеки Cabal;
- утилите
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
) - и можно работать.
Начнём с пустышки
Делаем следующее:
$ mkdir probe
$ cd probe
$ stack new
Команда new
создаёт простейший проект-пустышку в текущем каталоге:
$ ls
LICENSE
Setup.hs
app/
new-template.cabal
src/
stack.yaml
test/
Перед нами - самый обыкновенный Haskell-проект. Кстати, он прямо сейчас, без каких-либо дополнительных манипуляций готов к сборке, в отличие от проекта, создаваемого с помощью cabal init
.
Самая важная часть, которой не было раньше - файл stack.yaml
. Заглянем в него:
flags: {}
packages:
- '.'
extra-deps: []
resolver: lts-2.15
Обратите внимание на последнюю строчку, ибо она очень, очень важна. lts-2.15
- это версия нашей Haskell-экосистемы. LTS - от “Long Term Support” - представляет собой фиксированный набор Haskell-пакетов. Но что значит “фиксированный набор”? Поясню.
Stackage
Как мы знаем, наш любимый Hackage содержит очень много пакетов: на данный момент их чуть более 8400 штук. Но будет честными - далеко не всё из этой кучи пригодно для реального применения. Многие из пакетов - сугубо экспериментальные (читай “игрушечные”), а есть и вообще заброшенные. Поэтому друзья из FP Complete придумали Stackage, от “Stable Hackage”.
Идея в следующем: отобрать часть конкретных пакетов - и закрепить их в своего рода снимке (snapshot), присвоив этому снимку версию. Например, снимок версии 2.15 включает в себя 1066 пакетов, причём версия каждого из них явно указана (взгляните на список).
Именно благодаря этому мы избавляемся от упомянутой выше непредсказуемости. Версия lts
-снимка однозначно определяет версию каждого пакета, используемого в нашем проекте. Поэтому когда N программистов, работающих с неким проектом, используют одну и ту же версию lts
-снимка - у них всё гарантированно работает (как сегодня, так и завтра).
Это особенно полезно при расширении разработки. Пришёл новый человек в команду - никаких проблем с вечно обновляющимися пакетами: он просто берёт ту же версию снимка, что и все остальные коллеги, собирает - и у него всё ок.
Собираем
Пора собрать наш проект:
$ stack build
Downloaded lts-2.15 build plan.
Caching build plan
Fetched package index.
Populated index cache.
GHC version mismatched, found 7.8.3 (x86_64), but expected version 7.8.4 (x86_64) (based on resolver setting in /Users/dshevchenko/probe_stack/stack.yaml). Try running stack setup
В начале происходит загрузка “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 setup
Downloaded ghc-7.8.4.
Installed GHC.
Would add the following to PATH: /Users/dshevchenko/.stack/programs/x86_64-osx/ghc-7.8.4/bin
Немного терпения - и новый компилятор готов к использованию. Обратите внимание, что установлен он в скрытый каталог .stack
, созданный в вашем домашнем каталоге. Следовательно, никаких конфликтов с уже имеющимся компилятором не будет:
$ ghc --version
The Glorious Glasgow Haskell Compilation System, version 7.8.3
Новая версия 7.8.4 будет использоваться исключительно в процессе сборки нашего проекта. А кстати, вдруг я вас обманываю? Как мы можем проверить версию реально используемого компилятора? Очень просто, ведь stack
умеет запускать интерпретатор ghci
:
$ stack ghci
Configuring GHCi with the following packages: new-template
GHCi, version 7.8.4: http://www.haskell.org/ghc/ :? for help
...
Та-дам! Действительно 7.8.4.
Теперь уже точно собираем
Выполняем:
$ stack build
new-template-0.1.0.0: configure
Configuring new-template-0.1.0.0...
new-template-0.1.0.0: build
Building new-template-0.1.0.0...
Preprocessing library new-template-0.1.0.0...
[1 of 1] Compiling Lib ( src/Lib.hs, .stack-work/dist/x86_64-osx/Cabal-1.18.1.5/build/Lib.o )
In-place registering new-template-0.1.0.0...
Preprocessing executable 'new-template-exe' for new-template-0.1.0.0...
[1 of 1] Compiling Main ( app/Main.hs, .stack-work/dist/x86_64-osx/Cabal-1.18.1.5/build/new-template-exe/new-template-exe-tmp/Main.o )
Linking .stack-work/dist/x86_64-osx/Cabal-1.18.1.5/build/new-template-exe/new-template-exe ...
new-template-0.1.0.0: install
Installing library in
/Users/dshevchenko/probe_stack/.stack-work/install/x86_64-osx/lts-2.15/7.8.4/lib/x86_64-osx-ghc-7.8.4/new-template-0.1.0.0
Installing executable(s) in
/Users/dshevchenko/probe_stack/.stack-work/install/x86_64-osx/lts-2.15/7.8.4/bin
Registering new-template-0.1.0.0...
Как видите, в корне проекта появился скрытый каталог .stack-work
, в котором и осуществляется вся сборка. Взгляните, куда установилась наша программка:
~/.stack-work/install/x86_64-osx/lts-2.15/7.8.4/bin
Как видите, указана не только версия используемого компилятора, но и версия используемого lts
-снимка.
Проверяем версии пакетов
Итак, собрали. А теперь убедимся в правоте наших ожиданий в отношении версий используемых пакетов.
Раньше версии пакетов (или диапазоны версий) указывались в .cabal
-файле, в секции build-depends
:
-depends: split >=0.2 && <0.3,
build>=1.3 && <1.4,
filepath ...
Теперь же, как мы помним, версии пакетов определяются используемым нами lts
-снимком. Проверим это.
Я воспользуюсь одним из своих блогов, создаваемых, как многие знают, с помощью hakyll
. Проект зависит от маленькой библиотеки split
, помогающей разделывать строчки на кусочки. lts
-снимок версии 2.15 содержит указание версии:
...
split-0.2.2 Combinator library for splitting lists
...
Итак, используется 0.2.2. А теперь я сделаю вид, что напутал с версиями и указал в .cabal
-файле следующее:
-depends: split ==0.3.*,
build...
На самом деле, версия 0.2.2 является последней, но суть не в этом. Суть в том, что проект не соберётся, потому что stack
строг и аккуратен по отношению к версиям.
$ cd hand
$ stack init
Writing default config file to: /Users/dshevchenko/hand/stack.yaml
Basing on cabal files:
- /Users/dshevchenko/hand/hand.cabal
Checking against build plan lts-2.15
* Build plan did not match your requirements:
split version 0.2.2 found
- hand requires ==0.3.*
Первое, на что следует обратить внимание - новая команда. Мы используем stack init
, потому что собираемся использовать stack
для уже существующего Haskell-проекта. stack
создаёт в его корне уже знакомый нам файл stack.yaml
. И кстати, если в корне вашего проекта есть cabal
-песочницы или файл cabal.config
- смело удаляйте всё это.
Сразу после создания .yaml
-файла stack
проверяет готовность проекта быть собранным в соответствии с lts
-снимком версии 2.15. И тут - неприятный, но вполне ожидаемый результат:
* Build plan did not match your requirements:
split version 0.2.2 found
- hand requires ==0.3.*
Версии пакета в .lts
-снимке и в .cabal
-файле различаются, поэтому сборка не пройдёт. Решение - убрать указание версий из .cabal
-файла (ещё раз спасибо, Кэп!). Больше никакой путаницы - версии всех используемых пакетов явно указаны в lts
-снимке, и нигде более.
Больше компиляторов, хороших и разных
Ну что ж, раз можно поставить дополнительный “локальный” компилятор, исходя из версии экосистемы конкретного проекта, значит, можно поставить и несколько компиляторов.
Допустим, я хочу поэкспериментировать с новым GHC 7.10.1 с существующим проектом. Перехожу в проект:
$ cd new_hs_project
$ stack init
Теперь открываем stack.yaml
и заменяем последнюю строчку на:
resolver: nightly-2015-07-12
Оказывается, lts
-снимок - не единственная возможность указания Haskell-экосистемы. LTS прекрасно подходит для серьёзных долгоживущих проектов, но для экспериментаторов, предпочитающих быть на гребне волны, подойдёт одна из ночных сборок. В данном случае - сборка от 12 июля сего года. Живёт она здесь. Откроем её:
Stackage Nightly 2015-07-12 - GHC 7.10.1
То, что нам нужно! Раз данная сборка проверена с помощью 7.10.1, то при выполнении команды stack setup
произойдёт установка именно этой версии компилятора:
$ stack setup
Downloaded ghc-7.10.1.
Installed GHC.
Would add the following to PATH: /Users/dshevchenko/.stack/programs/x86_64-osx/ghc-7.10.1/bin
И теперь у нас есть два разных компилятора, не мешающих друг другу. Соответственно, все мои проекты, зависящие от 7.8.4, будут использовать только его, а проекты, зависящие от 7.10.1 - его. Никакой путаницы.
А куда он ставит пакеты?
До появляения cabal sandbox
все пакеты ставились в глобальное пространство, и это было ужасно. Идея песочницы частично решила проблему: каждый проект теперь скрыто включал свою собственную песочницу, в которую устанавливалось всё, необходимое для данного проекта.
Вроде бы неплохое решение, но оно грубое в силу своей избыточности. Представьте себе, что у вас на машине есть три yesod
-проекта. Соответственно, у вас появится три песочницы с тремя полными копиями всех пакетов, входящих в yesod
-зависимости (кто работал с yesod
, тот знает, что зависимостей этих, мягко выражаясь, много). Во-первых, попусту растрачивается дисковое пространство, а во-вторых, при появлении четвёртого yesod
-проекта (ну вот любите вы yesod
, почему бы и нет?) весь yesod
будет заново установлен в чертвёртую песочницу. И особенно обидно будет при условии, если во всех четырёх проектах используется одна и та же версия yesod
…
В stack
всё умнее. Больше никаких избыточных копий пакетов внутри каждого из проектов. Теперь пакеты устанавливаются в единое глобальное место, а именно в упомянутый ранее скрытый каталог .stack
в вашем домашнем каталоге. Но! Пакеты больше не валятся в одну кучу. Напротив, они устанавливаются иерархично, в соответствии с lts
-снимками (ну и ночными сборками, если таковые используются).
Заглянем:
$ ls ~/.stack/snapshots/x86_64-osx/
lts-2.15
nightly-2015-07-12
Вот они, наши снимки. Глянем поглубже:
$ ls ~/.stack/snapshots/x86_64-osx/lts-2.15/7.8.4/lib/x86_64-osx-ghc-7.8.4/
HTTP-4000.2.19 data-default-instances-containers-0.0.1 regex-tdfa-1.2.0
HUnit-1.2.5.2 data-default-instances-dlist-0.0.1 resourcet-1.1.5
JuicyPixels-3.2.5.2 data-default-instances-old-locale-0.0.1 rfc5051-0.1.0.3
MonadCatchIO-transformers-0.3.1.3 deepseq-generics-0.1.1.2 scientific-0.3.3.8
...
И вот они, все наши пакеты. Из пути сразу понятно, о каком снимке идёт речь и какая версия компилятора используется.
Таким образом, если упомянутые ранее три 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
выразит свой категорический протест:
-- While attempting to add dependency,
Could not find package string-qq in known packages
Поэтому открываем stack.yaml
, ищем строчку:
extra-deps: []
и заменяем её на:
extra-deps:
- string-qq-0.0.2
Готово. Теперь при сборке stack
увидит, что данный пакет нужно искать не в используемом снимке, а напрямую в Hackage. Соответственно, если мне вдруг понадобится более одного внешнего пакета, я просто дописываю их следом:
extra-deps:
- string-qq-0.0.2
- hfsevents-0.1.5
И всё заработает.
Установка в удобное место
Иногда после сборки проекта бывает удобно установить исполняемый файл приложения в какое-нибудь доступное место. Для этого служит команда install
:
$ stack install
Copying from /Users/dshevchenko/probe/.stack-work/install/x86_64-osx/lts-2.15/7.8.4/bin/new-template-exe to /Users/dshevchenko/.local/bin/new-template-exe
И если путь ~/.local/bin/
добавлен в PATH, то приложение будет немедленно доступно напрямую. Весьма удобно.
Вывод
Вывод только один - классную штуку сделали друзья из FP Complete. Лично я перевожу на stack
все мои проекты. Прощай, cabal
!
Кстати, если вы работаете на Linux - для вас хорошая новость: для Ubuntu, Debian, CentOS/Red Hat и Fedora уже готовы соответствующие пакеты. Ну а нам, маководам, осталось дождаться добавления stack
в brew
… ;-)
Полезные ссылки
- FP Complete про stack.
- Архитектура stack.
- Подробнее про stack.yaml.