aura-2.0.2: lib/Aura/Types.hs
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE MultiWayIf #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeApplications #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}
-- |
-- Module : Aura.Types
-- Copyright : (c) Colin Woodbury, 2012 - 2019
-- License : GPL3
-- Maintainer: Colin Woodbury <colin@fosskers.ca>
--
-- Core Aura types.
module Aura.Types
( -- * Package Types
Package(..), pname, pprov, pver, dividePkgs
, Dep(..), parseDep, renderedDep
, Buildable(..)
, Prebuilt(..)
, SimplePkg(..), simplepkg, simplepkg'
-- * Typeclasses
, Flagable(..)
-- * Package Building
, VersionDemand(..), _VersionDemand
, InstallType(..)
-- * Errors
, DepError(..)
, Failure(..)
-- * Language
, Language(..)
-- * Other Wrappers
, PkgName(..)
, PkgGroup(..)
, Provides(..)
, PackagePath(..)
, Pkgbuild(..)
, Environment
, User(..)
) where
import BasePrelude hiding (try)
import Control.Error.Util (hush)
import Data.Aeson (FromJSONKey, ToJSONKey)
import Data.Bitraversable
import qualified Data.ByteString.Lazy as BL
import Data.Generics.Product (field, super)
import qualified Data.Map.Strict as M
import Data.Semigroup.Foldable (Foldable1(..))
import Data.Set.NonEmpty (NESet)
import qualified Data.Set.NonEmpty as NES
import qualified Data.Text as T
import Data.Text.Prettyprint.Doc hiding (list, space)
import Data.Text.Prettyprint.Doc.Render.Terminal
import Data.These (These(..))
import Data.Versions hiding (Traversal')
import GHC.Generics (Generic)
import Lens.Micro
import System.Path (Absolute, Path, takeFileName, toUnrootedFilePath)
import Text.Megaparsec
import Text.Megaparsec.Char
---
-- | Types whose members can be converted to CLI flags.
class Flagable a where
asFlag :: a -> [T.Text]
instance Flagable T.Text where
asFlag t = [t]
instance (Foldable f, Flagable a) => Flagable (f a) where
asFlag = foldMap asFlag
-- | A package to be installed.
data Package = FromRepo Prebuilt | FromAUR Buildable deriving (Eq)
-- | The name of a `Package`.
pname :: Package -> PkgName
pname (FromRepo pb) = pb ^. field @"name"
pname (FromAUR b) = b ^. field @"name"
-- | Other names which allow this `Package` to be satisfied as a dependency.
pprov :: Package -> Provides
pprov (FromRepo pb) = pb ^. field @"provides"
pprov (FromAUR b) = b ^. field @"provides"
-- | The version of a `Package`.
pver :: Package -> Versioning
pver (FromRepo pb) = pb ^. field @"version"
pver (FromAUR b) = b ^. field @"version"
dividePkgs :: NESet Package -> These (NESet Prebuilt) (NESet Buildable)
dividePkgs = bimap NES.fromList NES.fromList . partNonEmpty f . NES.toList
where
f :: Package -> These Prebuilt Buildable
f (FromRepo p) = This p
f (FromAUR b) = That b
-- TODO Contribute this upstream.
-- | Partition a `NonEmpty` based on some function.
partNonEmpty :: (a -> These b c) -> NonEmpty a -> These (NonEmpty b) (NonEmpty c)
partNonEmpty f = foldMap1 (bimap pure pure . f)
-- TODO Figure out how to do this more generically.
instance Ord Package where
compare (FromAUR a) (FromAUR b) = compare a b
compare (FromRepo a) (FromRepo b) = compare a b
compare (FromAUR a) (FromRepo b) = compare (a ^. super @SimplePkg) (b ^. super @SimplePkg)
compare (FromRepo a) (FromAUR b) = compare (a ^. super @SimplePkg) (b ^. super @SimplePkg)
-- | A `Package` from the AUR that's buildable in some way on the user's machine.
data Buildable = Buildable { name :: !PkgName
, version :: !Versioning
, base :: !PkgName
, provides :: !Provides
, deps :: ![Dep]
, pkgbuild :: !Pkgbuild
, isExplicit :: !Bool } deriving (Eq, Ord, Show, Generic)
-- | A prebuilt `Package` from the official Arch repositories.
data Prebuilt = Prebuilt { name :: !PkgName
, version :: !Versioning
, base :: !PkgName
, provides :: !Provides } deriving (Eq, Ord, Show, Generic)
-- | A dependency on another package.
data Dep = Dep { name :: !PkgName
, demand :: !VersionDemand } deriving (Eq, Ord, Show, Generic)
-- | Parse a dependency entry as it would appear in a PKGBUILD:
--
-- @
-- >>> parseDep "pacman>1.2.3"
-- Just (Dep {name = PkgName {name = "pacman"}, demand = >1.2.3})
-- @
parseDep :: T.Text -> Maybe Dep
parseDep = hush . parse dep "dep"
where dep = Dep <$> n <*> v
n = PkgName <$> takeWhile1P Nothing (\c -> c /= '<' && c /= '>' && c /= '=')
v = do
end <- atEnd
if | end -> pure Anything
| otherwise -> choice [ char '<' *> fmap LessThan versioning'
, string ">=" *> fmap AtLeast versioning'
, char '>' *> fmap MoreThan versioning'
, char '=' *> fmap MustBe versioning'
, pure Anything ]
-- | Renders the `Dep` into a form that @pacman -T@ understands. The dual of
-- `parseDep`.
renderedDep :: Dep -> T.Text
renderedDep (Dep n ver) = (n ^. field @"name") <> asT ver
where
asT :: VersionDemand -> T.Text
asT (LessThan v) = "<" <> prettyV v
asT (AtLeast v) = ">=" <> prettyV v
asT (MoreThan v) = ">" <> prettyV v
asT (MustBe v) = "=" <> prettyV v
asT Anything = ""
-- | The versioning requirement of some package's dependency.
data VersionDemand = LessThan Versioning
| AtLeast Versioning
| MoreThan Versioning
| MustBe Versioning
| Anything
deriving (Eq, Ord)
instance Show VersionDemand where
show (LessThan v) = T.unpack $ "<" <> prettyV v
show (AtLeast v) = T.unpack $ ">=" <> prettyV v
show (MoreThan v) = T.unpack $ ">" <> prettyV v
show (MustBe v) = T.unpack $ "=" <> prettyV v
show Anything = "Anything"
-- | Attempt to zoom into the `Versioning` hiding within a `VersionDemand`.
_VersionDemand :: Traversal' VersionDemand Versioning
_VersionDemand f (LessThan v) = LessThan <$> f v
_VersionDemand f (AtLeast v) = AtLeast <$> f v
_VersionDemand f (MoreThan v) = MoreThan <$> f v
_VersionDemand f (MustBe v) = MustBe <$> f v
_VersionDemand _ p = pure p
-- | The installation method.
data InstallType = Pacman PkgName | Build Buildable deriving (Eq)
-- | A package name with its version number.
data SimplePkg = SimplePkg { name :: !PkgName, version :: !Versioning } deriving (Eq, Ord, Show, Generic)
-- | Attempt to create a `SimplePkg` from filepaths like
-- @\/var\/cache\/pacman\/pkg\/linux-3.2.14-1-x86_64.pkg.tar.xz@
simplepkg :: PackagePath -> Maybe SimplePkg
simplepkg (PackagePath t) = uncurry SimplePkg <$> bitraverse hush hush (parse n "name" t', parse v "version" t')
where t' = T.pack . toUnrootedFilePath $ takeFileName t
n :: Parsec Void T.Text PkgName
n = PkgName . T.pack <$> manyTill anySingle (try finished)
-- | Assumes that a version number will never start with a letter,
-- and that a package name section (i.e. abc-def-ghi) will never start
-- with a number.
finished = char '-' *> lookAhead digitChar
v = manyTill anySingle (try finished) *> ver
ver = try (fmap Ideal semver' <* post) <|> try (fmap General version' <* post) <|> fmap Complex mess'
post = char '-' *> (string "x86_64" <|> string "any") *> string ".pkg.tar.xz"
-- | Attempt to create a `SimplePkg` from text like:
-- xchat 2.8.8-19
simplepkg' :: T.Text -> Maybe SimplePkg
simplepkg' = hush . parse parser "name-and-version"
where parser = SimplePkg <$> (PkgName <$> takeWhile1P Nothing (/= ' ')) <*> (space *> versioning')
-- | Filepaths like:
--
-- * \/var\/cache\/pacman\/pkg\/linux-3.2.14-1-x86_64.pkg.tar.xz
-- * \/var\/cache\/pacman\/pkg\/wine-1.4rc6-1-x86_64.pkg.tar.xz
-- * \/var\/cache\/pacman\/pkg\/ruby-1.9.3_p125-4-x86_64.pkg.tar.xz
newtype PackagePath = PackagePath { path :: Path Absolute } deriving (Eq, Generic)
-- | If they have the same package names, compare by their versions.
-- Otherwise, do raw comparison of the path string.
instance Ord PackagePath where
compare a b | nameA /= nameB = compare (path a) (path b)
| otherwise = compare verA verB
where (nameA, verA) = f a
(nameB, verB) = f b
f = ((^? _Just . field @"name") &&& (^? _Just . field @"version")) . simplepkg
-- | The contents of a PKGBUILD file.
newtype Pkgbuild = Pkgbuild { pkgbuild :: BL.ByteString } deriving (Eq, Ord, Show, Generic)
-- | All human languages available for text output.
data Language = English
| Japanese
| Polish
| Croatian
| Swedish
| German
| Spanish
| Portuguese
| French
| Russian
| Italian
| Serbian
| Norwegian
| Indonesia
| Chinese
| Esperanto
deriving (Eq, Enum, Bounded, Ord, Show)
-- | The various ways that dependency resolution can fail.
data DepError = NonExistant PkgName
| VerConflict (Doc AnsiStyle)
| Ignored (Doc AnsiStyle)
| BrokenProvides PkgName Provides PkgName
-- | Some failure message that when given the current runtime `Language`
-- will produce a human-friendly error.
newtype Failure = Failure { failure :: Language -> Doc AnsiStyle }
-- | Shell environment variables.
type Environment = M.Map T.Text T.Text
-- | The name of a user account on a Linux system.
newtype User = User { user :: T.Text } deriving (Eq, Show, Generic)
-- | The name of an Arch Linux package.
newtype PkgName = PkgName { name :: T.Text }
deriving stock (Eq, Ord, Show, Generic)
deriving newtype (Flagable, ToJSONKey, FromJSONKey, IsString)
-- | A group that a `Package` could belong too, like @base@, @base-devel@, etc.
newtype PkgGroup = PkgGroup { group :: T.Text }
deriving stock (Eq, Ord, Show, Generic)
deriving newtype (Flagable)
-- | The dependency which some package provides. May not be the same name
-- as the package itself (e.g. cronie provides cron).
newtype Provides = Provides { provides :: PkgName } deriving (Eq, Ord, Show, Generic)