hercules-ci-agent-0.9.12: hercules-ci-agent/Hercules/Agent/Build.hs
{-# LANGUAGE CPP #-}
module Hercules.Agent.Build where
import Data.Aeson qualified as A
import Data.IORef.Lifted
import Data.Map qualified as M
import Hercules.API.Agent.Build qualified as API.Build
import Hercules.API.Agent.Build.BuildEvent qualified as BuildEvent
import Hercules.API.Agent.Build.BuildEvent.OutputInfo
( OutputInfo,
)
import Hercules.API.Agent.Build.BuildEvent.OutputInfo qualified as OutputInfo
import Hercules.API.Agent.Build.BuildEvent.Pushed qualified as Pushed
import Hercules.API.Agent.Build.BuildTask
( BuildTask,
)
import Hercules.API.Agent.Build.BuildTask qualified as BuildTask
import Hercules.API.Servant (noContent)
import Hercules.API.TaskStatus (TaskStatus)
import Hercules.API.TaskStatus qualified as TaskStatus
import Hercules.Agent.Cache qualified as Agent.Cache
#if ! MIN_VERSION_cachix(1, 4, 0) || MIN_VERSION_cachix(1, 5, 0)
import Hercules.Agent.Cachix.Env qualified as Cachix.Env
#endif
import Hercules.Agent.Client qualified
import Hercules.Agent.Config qualified as Config
import Hercules.Agent.Env
import Hercules.Agent.Env qualified as Env
import Hercules.Agent.Log
import Hercules.Agent.Nix qualified as Nix
import Hercules.Agent.Sensitive (Sensitive (Sensitive))
import Hercules.Agent.ServiceInfo qualified as ServiceInfo
import Hercules.Agent.WorkerProcess
import Hercules.Agent.WorkerProcess qualified as WorkerProcess
import Hercules.Agent.WorkerProtocol.Command qualified as Command
import Hercules.Agent.WorkerProtocol.Command.Build qualified as Command.Build
import Hercules.Agent.WorkerProtocol.Event qualified as Event
import Hercules.Agent.WorkerProtocol.Event.BuildResult qualified as BuildResult
import Hercules.Agent.WorkerProtocol.LogSettings qualified as LogSettings
import Hercules.CNix.Store qualified as CNix
import Hercules.Error (defaultRetry)
import Network.URI qualified
import Protolude
import System.Process
performBuild :: BuildTask.BuildTask -> App TaskStatus
performBuild buildTask = do
workerExe <- getWorkerExe
commandChan <- liftIO newChan
statusRef <- newIORef Nothing
extraNixOptions <- Nix.askExtraOptions
workerEnv <-
liftIO $
WorkerProcess.prepareEnv
( WorkerProcess.WorkerEnvSettings
{ nixPath = mempty,
extraEnv = mempty
}
)
let opts = [show extraNixOptions]
procSpec =
(System.Process.proc workerExe opts)
{ env = Just workerEnv,
close_fds = True,
cwd = Nothing
}
writeEvent :: Event.Event -> App ()
writeEvent event = case event of
Event.BuildResult r -> writeIORef statusRef $ Just r
Event.Exception e -> do
logLocM DebugS $ logStr (show e :: Text)
panic e
_ -> pass
baseURL <- asks (ServiceInfo.bulkSocketBaseURL . Env.serviceInfo)
materialize <- asks (not . Config.nixUserIsTrusted . Env.config)
liftIO $
writeChan commandChan $
Just $
Command.Build $
Command.Build.Build
{ drvPath = BuildTask.derivationPath buildTask,
inputDerivationOutputPaths = encodeUtf8 <$> BuildTask.inputDerivationOutputPaths buildTask,
logSettings =
LogSettings.LogSettings
{ token = Sensitive $ BuildTask.logToken buildTask,
path = "/api/v1/logs/build/socket",
baseURL = toS $ Network.URI.uriToString identity baseURL ""
},
materializeDerivation = materialize
}
let stderrHandler =
stderrLineHandler
( M.fromList
[ ("taskId", A.toJSON (BuildTask.id buildTask)),
("derivationPath", A.toJSON (BuildTask.derivationPath buildTask))
]
)
"Builder"
exitCode <- runWorker procSpec stderrHandler commandChan writeEvent
logLocM DebugS $ "Worker exit: " <> logStr (show exitCode :: Text)
case exitCode of
ExitSuccess -> pass
_ -> panic $ "Worker failed: " <> show exitCode
status <- readIORef statusRef
case status of
Just BuildResult.BuildSuccess {outputs = outs'} -> do
let outs = convertOutputs (BuildTask.derivationPath buildTask) outs'
reportOutputInfos buildTask outs
#if MIN_VERSION_cachix(1, 4, 0) && ! MIN_VERSION_cachix(1, 5, 0)
CNix.withStore $ \store -> push store buildTask outs
#else
asks (Cachix.Env.store . Env.cachixEnv) >>= \store -> push store buildTask outs
#endif
reportSuccess buildTask
pure $ TaskStatus.Successful ()
Just BuildResult.BuildFailure {} -> pure $ TaskStatus.Terminated ()
Nothing -> pure $ TaskStatus.Exceptional "Build did not complete"
convertOutputs :: Text -> [BuildResult.OutputInfo] -> Map Text OutputInfo
convertOutputs deriver = foldMap $ \oi ->
M.singleton (decodeUtf8With lenientDecode $ BuildResult.name oi) $
OutputInfo.OutputInfo
{ OutputInfo.deriver = deriver,
name = decodeUtf8With lenientDecode $ BuildResult.name oi,
path = decodeUtf8With lenientDecode $ BuildResult.path oi,
size = fromIntegral $ BuildResult.size oi,
hash = decodeUtf8With lenientDecode $ BuildResult.hash oi
}
push :: CNix.Store -> BuildTask -> Map Text OutputInfo -> App ()
push store buildTask outs = do
let paths = OutputInfo.path <$> toList outs
caches <- activePushCaches
forM_ caches $ \cache -> do
-- TODO preserve StorePath instead
storePaths <- liftIO $ for paths (CNix.parseStorePath store . encodeUtf8)
Agent.Cache.push store cache storePaths 4
emitEvents buildTask [BuildEvent.Pushed $ Pushed.Pushed {cache = cache}]
reportSuccess :: BuildTask -> App ()
reportSuccess buildTask = emitEvents buildTask [BuildEvent.Done True]
reportOutputInfos :: BuildTask -> Map Text OutputInfo -> App ()
reportOutputInfos buildTask outs =
emitEvents buildTask $ map BuildEvent.OutputInfo (toList outs)
emitEvents :: BuildTask -> [BuildEvent.BuildEvent] -> App ()
emitEvents buildTask =
noContent
. defaultRetry
. runHerculesClient
. API.Build.updateBuild
Hercules.Agent.Client.buildClient
(BuildTask.id buildTask)