packages feed

craft-0.0.0.1: src/Craft/Apt.hs

module Craft.Apt where

import           Control.Lens
import           Control.Monad
import           Formatting

import           Craft
import qualified Craft.Package as Package



apt :: PackageManager
apt =
  PackageManager
  { _pmGetter         = getAptPackage
  , _pmInstaller      = aptInstall
  , _pmUpgrader       = aptInstall
  , _pmUninstaller    = aptPurge
  }


aptGet :: [String] -> Craft ()
aptGet args = exec_ "apt-get" $ ["-q", "-y"] ++ args


update :: Craft ()
update = aptGet ["update"]


autoremove :: Craft ()
autoremove = aptGet ["autoremove"]


dpkgQuery :: [String] -> Craft ExecResult
dpkgQuery = exec dpkgQueryBin


dpkgQueryBin :: String
dpkgQueryBin = "dpkg-query"


dpkgQueryStatus :: String -> Craft ExecResult
dpkgQueryStatus pn = dpkgQuery ["-s", pn]


expectOutput :: String -> [String] -> Craft String
expectOutput cmd args = do
  r <- $stdoutOrError =<< exec cmd args
  when (null r) $ $craftError $ formatToString ("'"%string%"' returned an empty result!")
                                               (unwords $ cmd:args)
  return r


dpkgQueryShow :: String -> String -> Craft String
dpkgQueryShow pattern n = expectOutput dpkgQueryBin [ "--show", "--showformat", pattern, n ]


dpkgQueryVersion :: String -> Craft String
dpkgQueryVersion = dpkgQueryShow "${Version}"


dpkgQueryPackage :: String -> Craft String
dpkgQueryPackage = dpkgQueryShow "${Package}"


getAptPackage :: PackageName -> Craft (Maybe Package)
getAptPackage pn =
  dpkgQueryStatus pn >>= \case
    ExecFail _ -> return Nothing
    ExecSucc _ -> Just <$> do
      r <- dpkgQueryVersion pn
      return $ Package pn (Version r)


aptInstallArgs :: [String]
aptInstallArgs = ["-o", "DPkg::Options::=--force-confold", "install"]


aptInstall :: Package -> Craft ()
aptInstall pkg = aptGet $ aptInstallArgs ++ [pkgArg pkg]


aptInstallMult :: [Package] -> Craft ()
aptInstallMult []   = return ()
aptInstallMult pkgs = aptGet $ aptInstallArgs ++ map pkgArg pkgs


pkgArg :: Package -> String
pkgArg (Package pn AnyVersion)  = pn
pkgArg (Package pn Latest)      = pn
pkgArg (Package pn (Version v)) = pn ++ "=" ++ v


aptRemove :: Package -> Craft ()
aptRemove Package{..} = aptGet ["remove", _pkgName]


aptPurge :: Package -> Craft ()
aptPurge Package{..} = aptGet ["remove", _pkgName, "--purge"]


data Deb = Deb
  { _debFile :: File
  , _debPkg  :: Package
  }
  deriving (Eq, Show)
makeLenses ''Deb


debName :: Lens' Deb String
debName = debPkg . pkgName


debVersion :: Lens' Deb Version
debVersion = debPkg . pkgVersion


deb :: File -> Craft Deb
deb f = do
  pkg <- packageFromDebFile f
  return $ Deb f pkg


packageFromDebFile :: File -> Craft Package
packageFromDebFile f = do
  name    <- dpkgDebPackage f
  version <- dpkgDebVersion f
  return $ Package name (Version version)


dpkgInstall :: File -> Craft ()
dpkgInstall f =
  withEnvVar "DEBIAN_FRONTEND" "noninteractive" $
    exec_ "dpkg" ["-i", fromAbsFile $ f^.path]


dpkgDebBin :: String
dpkgDebBin = "dpkg-deb"


dpkgDebShow :: String -> File -> Craft String
dpkgDebShow pattern f =
  expectOutput dpkgDebBin ["--show", "--showformat", pattern, fromAbsFile $ f^.path]


dpkgDebVersion :: File -> Craft String
dpkgDebVersion = dpkgDebShow "${Version}"


dpkgDebPackage :: File -> Craft String
dpkgDebPackage = dpkgDebShow "${Package}"


instance Craftable Deb Deb where
  watchCraft d = do
    let name = d ^. debName
    let expectedVersion = d ^. debVersion
    let get = getAptPackage name
    let error' = formatToString ("craft Deb `"%shown%"` failed! "%string) d
    let installAndVerify = do
          dpkgInstall $ d ^. debFile
          get >>= \case
            Nothing -> $craftError $ error' "Package Not Found."
            Just installedPkg ->
              when (expectedVersion /= installedPkg ^. pkgVersion) $
                $craftError . error' $ formatToString ("Wrong Version: "%shown%" Expected: "%shown) (installedPkg ^. pkgVersion) expectedVersion
    get >>= \case
      Nothing  -> do
        installAndVerify
        return (Created, d)
      Just installedPkg ->
        if expectedVersion == installedPkg ^. pkgVersion
        then return (Unchanged, d)
        else do
          installAndVerify
          return (Updated, d)


-- |A function that installs a list of Apt packages,
-- |but that only runs "apt-get update" if a given package is not already installed.
-- TESTME
craftPackages :: [Package] -> Craft [Package]
craftPackages packages = do
  whenM ((any comparePackages . zip packages) <$> mapM (Package.get . _pkgName) packages)
    update
  craft packages
 where
  comparePackages (_, Nothing) = True
  comparePackages (Package _ expectedVersion, Just (Package _ actualVersion)) =
    case (expectedVersion, actualVersion) of
      (AnyVersion, _) -> False
      (Latest,     _) -> True
      (x,          y) -> x /= y