Проект как пакет: cabal, Haddock, PVP
Генератор работает, но живёт россыпью модулей. Пора оформить его как настоящий пакет: cabal-файл с библиотекой и исполняемым файлом, осмысленная модульная структура, Haddock-документация прямо в коде, и понимание, как версии и PVP позволяют экосистеме из тысяч пакетов вообще собираться. К концу урока ты сможешь прочитать чужой пакет с Hackage и понять, из чего он сложен.
Идея
До сих пор cabal build как-то работал, и мы не заглядывали в его конфиг. Зря: cabal-файл это карта проекта. Он отвечает на вопросы, которые иначе пришлось бы держать в голове: какие тут модули, что из чего собирается, от каких пакетов и каких их версий мы зависим.
cabal читает файл имя-пакета.cabal и из него понимает, что собирать. Внутри файл делится на секции (stanzas): одна библиотека, сколько угодно исполняемых файлов, тестов и бенчмарков.
Анатомия cabal-файла
Вот шапка нашего пакета. Метаданные плюс одна общая секция, которую переиспользуют остальные:
cabal-version: 3.0
name: our-press
version: 0.1.0.0
synopsis: A tiny static blog generator
license: MIT
author: BatSchool
build-type: Simple
common warnings
ghc-options: -Wall -Wno-name-shadowing
cabal-version: 3.0 в первой строке это не версия пакета, а версия синтаксиса самого файла: она включает удобства вроде common-секций. version: 0.1.0.0 это уже версия пакета, к ней вернёмся. common warnings это переиспользуемый блок настроек: -Wall включает все предупреждения компилятора, а -Wno-name-shadowing гасит одно конкретное, которое нам мешает. Любая секция ниже подключает это через import: warnings.
Библиотека
Сердце пакета это библиотека: переиспользуемый код, который видят и тесты, и исполняемый файл, и (если выложить) чужие проекты.
library
import: warnings
hs-source-dirs: src
default-language: GHC2021
exposed-modules: Press.Html.Internal
Press.Html
Press.Markup
Press.Convert
Press.Env
Press.Directory
Press.OptParse
Press.Render.Text
Press
build-depends: base >=4.17 && <5
, directory >=1.3 && <1.4
, filepath >=1.4 && <1.6
, text >=2.0 && <2.2
, optparse-applicative >=0.17 && <0.20
default-extensions: LambdaCase
Разберём поля. hs-source-dirs: src говорит, где искать исходники. default-language: GHC2021 это набор включённых по умолчанию расширений языка (современный базовый профиль; раньше писали Haskell2010). exposed-modules это публичный список модулей библиотеки.
Тут стоит остановиться. Помнишь, в уроке 26 мы прятали конструкторы за экспорт-листом модуля? У cabal есть второй, более грубый уровень той же границы: модуль можно сделать внутренним, перенеся его из exposed-modules в other-modules. Тогда чужой код не сможет даже импортировать его. Мы оставили Press.Html.Internal в exposed-modules сознательно (так его видят тесты), но в зрелых пакетах внутренности часто прячут именно на уровне cabal. Две границы работают вместе: экспорт-лист прячет имена внутри модуля, cabal прячет модуль целиком.
build-depends это список зависимостей, и здесь самое важное про версии. Каждая запись это не “пакет X”, а “пакет X в диапазоне версий”. base >=4.17 && <5 значит “любая base от 4.17 включительно до 5”. Зачем диапазон, поговорим через раздел.
default-extensions: LambdaCase включает расширение синтаксиса (\case) для всех модулей библиотеки, чтобы не писать прагму в каждом файле.
Исполняемый файл и остальные секции
executable our-press
import: warnings
hs-source-dirs: app
main-is: Main.hs
default-language: GHC2021
build-depends: base
, our-press
test-suite our-press-test
import: warnings
type: exitcode-stdio-1.0
hs-source-dirs: test
main-is: Spec.hs
other-modules: MarkupSpec
ConvertSpec
HtmlSpec
GoldenSpec
build-depends: base, our-press, directory, filepath
, hspec >=2.10, QuickCheck >=2.14
default-extensions: LambdaCase
Ключевой момент: executable и test-suite зависят от our-press, то есть от нашей же библиотеки. Это рекомендуемая раскладка: вся логика в библиотеке, а исполняемый файл это тонкая обёртка (вспомни трёхстрочный app/Main.hs). Так логику можно и протестировать (тест тоже подключает библиотеку), и переиспользовать. type: exitcode-stdio-1.0 означает, что тест это программа, которая завершается с кодом 0 при успехе и ненулевым при провале, ровно то, что делает hspec.
Запусти, и каждая секция строится своей командой:
cabal build # библиотека плюс исполняемый файл
cabal test # секция test-suite
cabal run our-press # секция executable
премиум-урок
Продолжение в премиуме: 1 200 ₽ в месяц или 10 000 ₽ в год. Вместе с ним серверные проверки задач и школьные токены AI-коуча.
Оплата пока не подключена: премиум и курсы открываются промокодом.