packages feed

arion-compose-0.2.2.0: src/haskell/lib/Arion/Nix.hs

module Arion.Nix
  ( evaluateComposition,
    withEvaluatedComposition,
    buildComposition,
    withBuiltComposition,
    replForComposition,
    EvaluationArgs (..),
    EvaluationMode (..),
  )
where

import Arion.Aeson (pretty)
import Control.Arrow ((>>>))
import Data.Aeson
import qualified Data.ByteString.Lazy as BL
import qualified Data.List.NonEmpty as NE
import qualified Data.String
import qualified Data.Text.IO as T
import Paths_arion_compose
import Protolude
import qualified System.Directory as Directory
import System.IO (hClose)
import System.IO.Temp (withTempFile)
import System.Process
import Prelude ()

data EvaluationMode
  = ReadWrite
  | ReadOnly

data EvaluationArgs = EvaluationArgs
  { posixUID :: Int,
    evalModulesFile :: NonEmpty FilePath,
    pkgsExpr :: Text,
    workDir :: Maybe FilePath,
    mode :: EvaluationMode,
    extraNixArgs :: [Text]
  }

evaluateComposition :: EvaluationArgs -> IO Value
evaluateComposition ea = do
  evalComposition <- getEvalCompositionFile
  let commandArgs =
        [ "--eval",
          "--strict",
          "--json",
          "--attr",
          "config.out.dockerComposeYamlAttrs"
        ]
      args =
        [evalComposition]
          ++ commandArgs
          ++ modeArguments ea.mode
          ++ argArgs ea
          ++ map toS ea.extraNixArgs
      procSpec =
        (proc "nix-instantiate" args)
          { cwd = ea.workDir,
            std_out = CreatePipe
          }

  withCreateProcess procSpec $ \_in outHM _err procHandle -> do
    let outHandle = fromMaybe (panic "stdout missing") outHM

    out <- BL.hGetContents outHandle

    v <- Protolude.evaluate (eitherDecode out)

    exitCode <- waitForProcess procHandle

    case exitCode of
      ExitSuccess -> pass
      ExitFailure 1 -> exitFailure
      ExitFailure {} -> do
        throwIO $ FatalError $ "evaluation failed with " <> show exitCode

    case v of
      Right r -> pure r
      Left e -> throwIO $ FatalError ("Couldn't parse nix-instantiate output" <> show e)

-- | Run with docker-compose.yaml tmpfile
withEvaluatedComposition :: EvaluationArgs -> (FilePath -> IO r) -> IO r
withEvaluatedComposition ea f = do
  v <- evaluateComposition ea
  withTempFile "." ".tmp-arion-docker-compose.yaml" $ \path yamlHandle -> do
    T.hPutStrLn yamlHandle (pretty v)
    hClose yamlHandle
    f path

buildComposition :: FilePath -> EvaluationArgs -> IO ()
buildComposition outLink ea = do
  evalComposition <- getEvalCompositionFile
  let commandArgs =
        [ "--attr",
          "config.out.dockerComposeYaml",
          "--out-link",
          outLink
        ]
      args =
        [evalComposition]
          ++ commandArgs
          ++ argArgs ea
          ++ map toS ea.extraNixArgs
      procSpec = (proc "nix-build" args) {cwd = ea.workDir}

  withCreateProcess procSpec $ \_in _out _err procHandle -> do
    exitCode <- waitForProcess procHandle

    case exitCode of
      ExitSuccess -> pass
      ExitFailure 1 -> exitFailure
      ExitFailure {} -> do
        throwIO $ FatalError $ "nix-build failed with " <> show exitCode

-- | Do something with a docker-compose.yaml.
withBuiltComposition :: EvaluationArgs -> (FilePath -> IO r) -> IO r
withBuiltComposition ea f = do
  withTempFile "." ".tmp-arion-docker-compose.yaml" $ \path emptyYamlHandle -> do
    hClose emptyYamlHandle
    -- Known problem: kills atomicity of withTempFile; won't fix because we should manage gc roots,
    -- impl of which will probably avoid this "problem". It seems unlikely to cause issues.
    Directory.removeFile path
    buildComposition path ea
    f path

replForComposition :: EvaluationArgs -> IO ()
replForComposition ea = do
  evalComposition <- getEvalCompositionFile
  let args =
        ["repl", "--file", evalComposition]
          ++ argArgs ea
          ++ map toS ea.extraNixArgs
      procSpec = (proc "nix" args) {cwd = ea.workDir}

  withCreateProcess procSpec $ \_in _out _err procHandle -> do
    exitCode <- waitForProcess procHandle

    case exitCode of
      ExitSuccess -> pass
      ExitFailure 1 -> exitFailure
      ExitFailure {} -> do
        throwIO $ FatalError $ "nix repl failed with " <> show exitCode

argArgs :: EvaluationArgs -> [[Char]]
argArgs ea =
  [ "--argstr",
    "uid",
    show ea.posixUID,
    "--arg",
    "modules",
    modulesNixExpr ea.evalModulesFile,
    "--arg",
    "pkgs",
    toS ea.pkgsExpr
  ]

getEvalCompositionFile :: IO FilePath
getEvalCompositionFile = getDataFileName "nix/eval-composition.nix"

modeArguments :: EvaluationMode -> [[Char]]
modeArguments ReadWrite = ["--read-write-mode"]
modeArguments ReadOnly = ["--readonly-mode"]

modulesNixExpr :: NonEmpty FilePath -> [Char]
modulesNixExpr =
  NE.toList >>> fmap pathExpr >>> Data.String.unwords >>> wrapList
  where
    pathExpr :: FilePath -> [Char]
    pathExpr path
      | isAbsolute path = "(/. + \"/${" <> toNixStringLiteral path <> "}\")"
      | otherwise = "(./. + \"/${" <> toNixStringLiteral path <> "}\")"

    isAbsolute ('/' : _) = True
    isAbsolute _ = False

    wrapList s = "[ " <> s <> " ]"

toNixStringLiteral :: [Char] -> [Char]
toNixStringLiteral = show -- FIXME: custom escaping including '$'