190 lines
6.4 KiB
Haskell
190 lines
6.4 KiB
Haskell
|
{-# LANGUAGE DerivingStrategies #-}
|
||
|
{-# LANGUAGE FlexibleContexts #-}
|
||
|
{-# LANGUAGE LambdaCase #-}
|
||
|
{-# LANGUAGE NamedFieldPuns #-}
|
||
|
{-# LANGUAGE NumericUnderscores #-}
|
||
|
{-# LANGUAGE OverloadedStrings #-}
|
||
|
{-# LANGUAGE QuasiQuotes #-}
|
||
|
{-# LANGUAGE ScopedTypeVariables #-}
|
||
|
{-# OPTIONS_GHC -Wall #-}
|
||
|
|
||
|
module Main where
|
||
|
|
||
|
import Conduit
|
||
|
import qualified Conduit as Cond
|
||
|
import Control.Concurrent
|
||
|
import qualified Control.Concurrent.Async as Async
|
||
|
import Control.Monad
|
||
|
import qualified Data.Aeson.BetterErrors as Json
|
||
|
import Data.Bifunctor
|
||
|
import Data.ByteString (ByteString)
|
||
|
import qualified Data.Conduit.Binary as Conduit.Binary
|
||
|
import qualified Data.Conduit.Combinators as Cond
|
||
|
import Data.Conduit.Process
|
||
|
import Data.Error
|
||
|
import Data.Function
|
||
|
import Data.Functor
|
||
|
import Data.List.NonEmpty (NonEmpty ((:|)), nonEmpty)
|
||
|
import Data.Text (Text)
|
||
|
import qualified Data.Text as Text
|
||
|
import qualified Data.Text.Encoding
|
||
|
import qualified Data.Text.Encoding.Error
|
||
|
import Data.Text.IO (hPutStrLn)
|
||
|
import PyF
|
||
|
import qualified System.Directory as Dir
|
||
|
import qualified System.Environment as Env
|
||
|
import qualified System.Exit as Exit
|
||
|
import System.FilePath (takeDirectory)
|
||
|
import qualified System.FilePath.Posix as FilePath
|
||
|
import System.IO (stderr)
|
||
|
import qualified System.Posix as Posix
|
||
|
import Prelude hiding (log)
|
||
|
|
||
|
data LorriEvent = LorriEvent
|
||
|
{ nixFile :: Text,
|
||
|
eventType :: LorriEventType
|
||
|
}
|
||
|
deriving stock (Show)
|
||
|
|
||
|
data ChanToken a
|
||
|
= -- | so we can see that the lorri thread has been initialized
|
||
|
NoEventYet
|
||
|
| ChanEvent a
|
||
|
|
||
|
data LorriEventType
|
||
|
= Completed
|
||
|
| Started
|
||
|
| Failure
|
||
|
deriving stock (Show)
|
||
|
|
||
|
main :: IO ()
|
||
|
main = do
|
||
|
argv <- Env.getArgs <&> nonEmpty
|
||
|
|
||
|
dir <- Dir.getCurrentDirectory
|
||
|
shellNix <-
|
||
|
findShellNix dir >>= \case
|
||
|
Nothing -> Exit.die [fmt|could not find any shell.nix in or above the directory {dir}|]
|
||
|
Just s -> pure s
|
||
|
getEventChan :: MVar (Chan LorriEvent) <- newEmptyMVar
|
||
|
Async.race_
|
||
|
( do
|
||
|
sendEventChan :: Chan LorriEvent <- newChan
|
||
|
(exitCode, ()) <-
|
||
|
sourceProcessWithConsumer
|
||
|
(proc "lorri" ["internal", "stream-events"])
|
||
|
$
|
||
|
-- first, we want to send a message over the chan that the process is running (for timeout)
|
||
|
liftIO (putMVar getEventChan sendEventChan)
|
||
|
*> Conduit.Binary.lines
|
||
|
.| Cond.mapC
|
||
|
( \jsonBytes ->
|
||
|
(jsonBytes :: ByteString)
|
||
|
& Json.parseStrict
|
||
|
( Json.key
|
||
|
"Completed"
|
||
|
( do
|
||
|
nixFile <- Json.key "nix_file" Json.asText
|
||
|
pure LorriEvent {nixFile, eventType = Completed}
|
||
|
)
|
||
|
Json.<|> Json.key
|
||
|
"Started"
|
||
|
( do
|
||
|
nixFile <- Json.key "nix_file" Json.asText
|
||
|
pure LorriEvent {nixFile, eventType = Started}
|
||
|
)
|
||
|
Json.<|> Json.key
|
||
|
"Failure"
|
||
|
( do
|
||
|
nixFile <- Json.key "nix_file" Json.asText
|
||
|
pure LorriEvent {nixFile, eventType = Failure}
|
||
|
)
|
||
|
)
|
||
|
& first Json.displayError'
|
||
|
& first (map newError)
|
||
|
& first (smushErrors [fmt|Cannot parse line returned by lorri: {jsonBytes & bytesToTextUtf8Lenient}|])
|
||
|
& unwrapError
|
||
|
)
|
||
|
.| (Cond.mapM_ (\ev -> writeChan sendEventChan ev))
|
||
|
|
||
|
log [fmt|lorri internal stream-events exited {show exitCode}|]
|
||
|
)
|
||
|
( do
|
||
|
let waitMs ms = threadDelay (ms * 1000)
|
||
|
|
||
|
-- log [fmt|Waiting for lorri event for {shellNix}|]
|
||
|
|
||
|
eventChan <- takeMVar getEventChan
|
||
|
|
||
|
let isOurEvent ev = FilePath.normalise (ev & nixFile & textToString) == FilePath.normalise shellNix
|
||
|
|
||
|
let handleEvent ev =
|
||
|
case ev & eventType of
|
||
|
Started ->
|
||
|
log [fmt|waiting for lorri build to finish|]
|
||
|
Completed -> do
|
||
|
log [fmt|build completed|]
|
||
|
exec (inDirenvDir (takeDirectory shellNix) <$> argv)
|
||
|
Failure -> do
|
||
|
log [fmt|evaluation failed! for path {ev & nixFile}|]
|
||
|
Exit.exitWith (Exit.ExitFailure 111)
|
||
|
|
||
|
-- wait for 100ms for the first message from lorri,
|
||
|
-- or else assume lorri is not building the project yet
|
||
|
Async.race
|
||
|
(waitMs 100)
|
||
|
( do
|
||
|
-- find the first event that we can use
|
||
|
let go = do
|
||
|
ev <- readChan eventChan
|
||
|
if isOurEvent ev then pure ev else go
|
||
|
go
|
||
|
)
|
||
|
>>= \case
|
||
|
Left () -> do
|
||
|
log [fmt|No event received from lorri, assuming this is the first evaluation|]
|
||
|
exec argv
|
||
|
Right ch -> handleEvent ch
|
||
|
|
||
|
runConduit $
|
||
|
repeatMC (readChan eventChan)
|
||
|
.| filterC isOurEvent
|
||
|
.| mapM_C handleEvent
|
||
|
)
|
||
|
where
|
||
|
inDirenvDir dir' argv' = ("direnv" :| ["exec", dir']) <> argv'
|
||
|
exec = \case
|
||
|
Just (exe :| args') -> Posix.executeFile exe True args' Nothing
|
||
|
Nothing -> Exit.exitSuccess
|
||
|
|
||
|
log :: Text -> IO ()
|
||
|
log msg = hPutStrLn stderr [fmt|lorri-wait-for-eval: {msg}|]
|
||
|
|
||
|
-- | Searches from the current directory upwards, until it finds the `shell.nix`.
|
||
|
findShellNix :: FilePath -> IO (Maybe FilePath)
|
||
|
findShellNix curDir = do
|
||
|
let go :: (FilePath -> IO (Maybe FilePath))
|
||
|
go dir = do
|
||
|
let file = dir FilePath.</> "shell.nix"
|
||
|
Dir.doesFileExist file >>= \case
|
||
|
True -> pure (Just file)
|
||
|
False -> pure Nothing
|
||
|
go curDir
|
||
|
|
||
|
textToString :: Text -> String
|
||
|
textToString = Text.unpack
|
||
|
|
||
|
smushErrors :: Foldable t => Text -> t Error -> Error
|
||
|
smushErrors msg errs =
|
||
|
errs
|
||
|
-- hrm, pretty printing and then creating a new error is kinda shady
|
||
|
& foldMap (\err -> "\n- " <> prettyError err)
|
||
|
& newError
|
||
|
& errorContext msg
|
||
|
|
||
|
-- | decode a Text from a ByteString that is assumed to be UTF-8,
|
||
|
-- replace non-UTF-8 characters with the replacment char U+FFFD.
|
||
|
bytesToTextUtf8Lenient :: Data.ByteString.ByteString -> Data.Text.Text
|
||
|
bytesToTextUtf8Lenient =
|
||
|
Data.Text.Encoding.decodeUtf8With Data.Text.Encoding.Error.lenientDecode
|