Раздел 21 · Haskell

Проект как пакет: cabal, Haddock, PVP

middle~30 мин

без входаВойди, чтобы отмечать прогресс, вести заметки и решать задачи в редакторе. войти

Проект как пакет: 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-коуча.

Оплата пока не подключена: премиум и курсы открываются промокодом.