packages feed

hnix-store-aterm-0.1.0.0: src/System/Nix/Derivation/Traditional.hs

{-# LANGUAGE DeriveGeneric     #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE KindSignatures    #-}
{-# LANGUAGE LambdaCase        #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE NamedFieldPuns    #-}
{-# LANGUAGE RankNTypes        #-}
{-# LANGUAGE RecordWildCards   #-}
{-# LANGUAGE ScopedTypeVariables  #-}
{-# LANGUAGE TypeApplications  #-}

-- | Shared types

module System.Nix.Derivation.Traditional
    ( RawDerivationOutput(..)
    , parseRawDerivationOutput
    , renderRawDerivationOutput
    , TraditionalDerivation'(..)
    , withName
    , withoutName
    , TraditionalDerivationInputs(..)
    , inputsToTraditional
    , inputsFromTraditional
    ) where


import Control.DeepSeq (NFData(..))
import Data.Constraint.Extras (Has(has))
import Data.Dependent.Sum (DSum(..))
import Data.Map (Map)
import Data.Map qualified
import Data.Map.Monoidal (MonoidalMap(..))
import Data.Map.Monoidal qualified
import Data.Maybe (fromMaybe)
import Data.Set (Set)
import Data.Some
import Data.Text (Text)
import Data.Text qualified
import Data.These (These(..))
import Data.Vector (Vector)
import Data.Traversable (for)
import GHC.Generics (Generic, (:.:)(..))

import System.Nix.ContentAddress (ContentAddressMethod(..))
import System.Nix.Derivation
import System.Nix.Hash
import System.Nix.OutputName (OutputName)
import System.Nix.StorePath

-- | Useful for the ATerm format, and remote protocols that need the same parsing
-- If it won't for the protocol, we would just inline this into the ATerm code proper.
data RawDerivationOutput = RawDerivationOutput
    { rawPath :: Text
    , rawMethodHashAlgo :: Text
    , rawHash :: Text
    } deriving (Eq, Generic, Ord, Show)

parseRawDerivationOutput
  :: forall m
  .  MonadFail m
  => StoreDir
  -> RawDerivationOutput
  -> m FreeformDerivationOutput
parseRawDerivationOutput storeDir (RawDerivationOutput {..}) = do
  let onNonEmptyText :: Text -> (Text -> m a) -> m (Maybe a)
      onNonEmptyText = flip $ \f -> \case
        "" -> pure Nothing
        t -> Just <$> f t
  mPath <- onNonEmptyText rawPath $ \t -> case System.Nix.StorePath.parsePathFromText storeDir t of
    Left e -> fail $ show e -- TODO
    Right sp -> pure sp
  mMethodHashAlgo <- onNonEmptyText rawMethodHashAlgo splitMethodHashAlgo
  mHash0 <- onNonEmptyText rawHash pure
  mContentAddressing <- case mMethodHashAlgo of
    Nothing -> case mHash0 of
      Nothing -> pure Nothing
      Just _ -> fail "Hash without method and hash algo is not allowed"
    Just (method, Some hashAlgo) -> do
      mHash <- for mHash0 $ \hash0 ->
        either fail pure $ has @NamedAlgo hashAlgo $
          decodeDigestWith NixBase32 hash0
      pure $ Just (method, hashAlgo :=> Comp1 mHash)
  pure FreeformDerivationOutput { mPath, mContentAddressing }

renderRawDerivationOutput
    :: StoreDir
    -> FreeformDerivationOutput
    -> RawDerivationOutput
renderRawDerivationOutput storeDir (FreeformDerivationOutput {..}) =
  RawDerivationOutput
    { rawPath = fromMaybe "" $ storePathToText storeDir <$> mPath
    , rawMethodHashAlgo = flip (maybe "") mContentAddressing $ \(method, hashAlgo :=> _) ->
        buildMethodHashAlgo method $ Some hashAlgo
    , rawHash = fromMaybe "" $ mContentAddressing >>= \(_, _ :=> Comp1 hash') ->
        encodeDigestWith NixBase32 <$> hash'
    }

buildMethodHashAlgo :: ContentAddressMethod -> Some HashAlgo -> Text
buildMethodHashAlgo method hashAlgo = Data.Text.intercalate ":" $
  (case method of
    ContentAddressMethod_NixArchive -> ["r"]
    ContentAddressMethod_Text -> ["text"]
    ContentAddressMethod_Flat -> [])
  <>
  [withSome hashAlgo algoToText]

splitMethodHashAlgo :: MonadFail m => Text -> m (ContentAddressMethod, Some HashAlgo)
splitMethodHashAlgo methodHashAlgo = do
  (method, hashAlgoS) <- case Data.Text.splitOn ":" methodHashAlgo of
    ["r", hashAlgo] -> pure (ContentAddressMethod_NixArchive, hashAlgo)
    ["text", hashAlgo] -> pure (ContentAddressMethod_Text, hashAlgo)
    [hashAlgo] -> pure (ContentAddressMethod_Flat, hashAlgo)
    _ -> fail "invalid number of colons or unknown CA method prefix"
  hashAlgo <- either fail pure $ textToAlgo hashAlgoS
  pure (method, hashAlgo)

----------------

-- | The ATerm format doesn't include the derivation name. That must
-- instead be gotten out of band, e.g. from the Store Path.
data TraditionalDerivation' inputs outputs = TraditionalDerivation
    { anonOutputs   :: outputs
    -- ^ Outputs produced by this derivation where keys are output names
    , anonInputs    :: inputs
    -- ^ Inputs (sources and derivations)
    , anonPlatform  :: Text
    -- ^ Platform required for this derivation
    , anonBuilder   :: Text
    -- ^ Code to build the derivation, which can be a path or a builtin function
    , anonArgs      :: Vector Text
    -- ^ Arguments passed to the executable used to build to derivation
    , anonEnv       :: Map Text Text
    -- ^ Environment variables provided to the executable used to build the
    -- derivation
    } deriving (Eq, Generic, Ord, Show)

instance (NFData inputs, NFData outputs) => NFData (TraditionalDerivation' inputs outputs)

withName :: StorePathName -> TraditionalDerivation' inputs outputs -> Derivation' inputs outputs
withName name drv0 = Derivation
  { name = name
  , outputs = anonOutputs drv0
  , inputs = anonInputs drv0
  , platform = anonPlatform drv0
  , builder = anonBuilder drv0
  , args = anonArgs drv0
  , env = anonEnv drv0
  }

withoutName :: Derivation' inputs outputs -> TraditionalDerivation' inputs outputs
withoutName drv0 = TraditionalDerivation
  { anonOutputs = outputs drv0
  , anonPlatform = platform drv0
  , anonInputs = inputs drv0
  , anonBuilder = builder drv0
  , anonArgs = args drv0
  , anonEnv = env drv0
  }

----------------

-- | Useful for the ATerm format
data TraditionalDerivationInputs = TraditionalDerivationInputs
    { traditionalSrcs :: Set StorePath
    -- ^ Inputs that are sources
    , traditionalDrvs :: Map StorePath (Set OutputName)
    -- ^ Inputs that are derivations where keys specify derivation paths and
    -- values specify which output names are used by this derivation
    } deriving (Eq, Generic, Ord, Show)

instance NFData TraditionalDerivationInputs

instance Semigroup TraditionalDerivationInputs where
  TraditionalDerivationInputs x0 x1 <> TraditionalDerivationInputs y0 y1 = TraditionalDerivationInputs
    (x0 <> y0)
    (x1 <> y1)

instance Monoid TraditionalDerivationInputs where
  mempty = TraditionalDerivationInputs mempty mempty

inputsToTraditional :: DerivationInputs -> Either StorePath TraditionalDerivationInputs
inputsToTraditional is = (\drvs -> TraditionalDerivationInputs
  { traditionalSrcs = srcs is
  , traditionalDrvs = drvs
  }) <$> go (drvs is)
  where
   go = fmap getMonoidalMap
     . Data.Map.Monoidal.traverseWithKey
       (\storePath -> (\case
           This os -> Right os
           _ -> Left storePath -- TODO make better error, e.g. by partitioning the map
        ) . unChildNode)
     . unDerivedPathMap

inputsFromTraditional :: TraditionalDerivationInputs -> DerivationInputs
inputsFromTraditional TraditionalDerivationInputs { traditionalSrcs, traditionalDrvs } = DerivationInputs
  { srcs = traditionalSrcs
  , drvs = DerivedPathMap $ Data.Map.Monoidal.fromList $
      fmap (fmap ChildNode . fmap This) (Data.Map.toList traditionalDrvs)
  }