build-env-1.2.0.0: src/BuildEnv/File.hs
{-# LANGUAGE TypeApplications #-}
-- |
-- Module : BuildEnv.File
-- Description : Parse packages and units from files
--
-- This module implements the parsing of the two file formats supported
-- by @build-env@:
--
-- - SEED files, containing a list of seed units from which to compute
-- a build plan. See 'parseSeedFile'.
--
-- - @cabal.config@ files containing version constraints on packages.
-- See 'parseCabalDotConfigPkgs'.
module BuildEnv.File
( parseCabalDotConfigPkgs, parseSeedFile )
where
-- base
import Data.Char
( isSpace )
-- containers
import qualified Data.Map.Strict as Map
import qualified Data.Set as Set
-- text
import Data.Text
( Text )
import qualified Data.Text as Text
import qualified Data.Text.IO as Text
-- build-env
import BuildEnv.CabalPlan
--------------------------------------------------------------------------------
-- | Parse constrained packages from the @constraints@ stanza
-- of the @cabal.config@ file at the given filepath:
--
-- > constraints: pkg1 ==ver1,
-- > pkg2 ==ver2,
-- > ...
--
-- This function disregards all other contents of the @cabal.config@ package.
parseCabalDotConfigPkgs :: FilePath -> IO PkgSpecs
parseCabalDotConfigPkgs fp = do
ls <- filter ( not . isCommentLine . Text.strip )
. Text.lines
<$> Text.readFile fp
return $ outsideStanza Map.empty ls
where
outsideStanza :: PkgSpecs -> [Text] -> PkgSpecs
outsideStanza pkgs []
= pkgs
outsideStanza pkgs (l:ls)
| Just rest <- Text.strip <$> Text.stripPrefix "constraints:" l
= inConstraintsStanza (pkgs `addPkgFromLine` rest) ls
| otherwise
= outsideStanza pkgs ls
inConstraintsStanza :: PkgSpecs -> [Text] -> PkgSpecs
inConstraintsStanza pkgs []
= pkgs
inConstraintsStanza pkgs (l:ls)
| let (ws, rest) = Text.span isSpace l
, not $ Text.null ws
= inConstraintsStanza (pkgs `addPkgFromLine` rest) ls
| otherwise
= outsideStanza pkgs (l:ls)
addPkgFromLine :: PkgSpecs -> Text -> PkgSpecs
addPkgFromLine pkgs l =
let (pkgName, pkgSpec) = parseCabalDotConfigLine l
in Map.insert pkgName pkgSpec pkgs
-- | Parse a 'PkgName' and 'PkgSpec' from a line in a @cabal.config@ file.
--
-- Assumes whitespace has already been stripped.
parseCabalDotConfigLine :: Text -> (PkgName, PkgSpec)
parseCabalDotConfigLine txt
| let (pkg, rest)
= Text.break isSpace
$ Text.dropAround (',' ==) -- drop commas
$ txt
, validPackageName pkg
= ( PkgName pkg, parsePkgSpec rest )
| otherwise
= error $ "Invalid package in cabal.config file: " <> Text.unpack txt
-- NB: update the readme after changing the documentation below.
-- | Parse a seed file. Each line must either be:
--
-- - A Cabal unit, in the format @unit +flag1 -flag2 >= 0.1 && < 0.3@.
--
-- A unit can be of the form @pkgName@, @lib:pkgName@, @exe:pkgName@,
-- @pkgName:lib:compName@, ... as per Cabal component syntax.
--
-- The unit name must be followed by a space.
--
-- Flags and constraints are optional.
-- When both are present, flags must precede constraints.
-- Constraints must use valid Cabal constraint syntax.
--
-- - An allow-newer specification, e.g. @allow-newer: pkg1:pkg2,*:base,...@.
-- This is not allowed to span multiple lines.
--
-- Returns @(units, allowNewer)@.
parseSeedFile :: FilePath -> IO (UnitSpecs, AllowNewer)
parseSeedFile fp = do
ls <- filter ( not . isCommentLine )
. map Text.strip
. Text.lines
<$> Text.readFile fp
return $ go Map.empty mempty ls
where
go :: UnitSpecs -> AllowNewer -> [Text] -> (UnitSpecs, AllowNewer)
go units ans [] = (units, ans)
go units ans (l:ls)
| Just an <- Text.stripPrefix "allow-newer:" l
= go units (ans <> parseAllowNewer an) ls
| let (pkgTyComp, rest) = Text.break isSpace l
, Just (pkgName, comp) <- parsePkgComponent pkgTyComp
, let spec = parsePkgSpec rest
thisUnit = Map.singleton pkgName
(Remote, spec, Set.singleton comp)
-- we assume units in a seed file
-- don't refer to local packages
= go (units `unionUnitSpecsCombining` thisUnit) ans ls
| otherwise
= error $ "Invalid package in seed file : " <> Text.unpack l
isCommentLine :: Text -> Bool
isCommentLine l
= Text.null l
|| Text.isPrefixOf "--" l
parseAllowNewer :: Text -> AllowNewer
parseAllowNewer l =
AllowNewer $ Set.fromList $ map parseOneAllowNewer (Text.splitOn "," l)
where
parseOneAllowNewer t
| (Text.strip -> a, Text.strip . Text.drop 1 -> b) <- Text.breakOn ":" t
, a == "*" || validPackageName a
, b == "*" || validPackageName b
= (a,b)
| otherwise
= error $ "Invalid allow-newer syntax in seed file: " <> Text.unpack t