packages feed

hwm-0.5.0: src/HWM/Integrations/Toolchain/Nix/Build.hs

{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE NoImplicitPrelude #-}

module HWM.Integrations.Toolchain.Nix.Build
  ( genNixBinary,
    nixBuild,
    inNixDevelop,
    NixEnv (..),
  )
where

import Control.Monad.Error.Class (MonadError)
import Control.Monad.Except (throwError)
import HWM.Core.Common (Name)
import HWM.Core.Formatting (Format (..), toCamelCase)
import HWM.Core.Pkg (Pkg (..), PkgName)
import HWM.Core.Result (Issue)
import HWM.Domain.Schema (TargetScope (..))
import HWM.Runtime.Process (Exec (..), mkExec)
import Relude
import System.Directory (copyFile, doesFileExist, doesPathExist, emptyPermissions, removePathForcibly, setOwnerExecutable, setOwnerReadable, setOwnerWritable, setPermissions)
import System.FilePath ((</>))

forNixLink :: (MonadIO m, MonadError Issue m) => FilePath -> (FilePath -> m ()) -> m ()
forNixLink dir f = do
  let linkPath = dir </> "result"
  isLink <- liftIO $ doesPathExist linkPath
  unless isLink
    $ throwError
    $ fromString
      ( "Nix build completed, but did not create an output at: "
          <> linkPath
          <> "\n(This usually means the Nix derivation is empty or build failed silently.)"
      )
  f linkPath
  liftIO $ removePathForcibly linkPath

copyBinary :: (MonadIO m) => FilePath -> FilePath -> m ()
copyBinary fromPath toPath = liftIO $ do
  liftIO $ copyFile fromPath toPath
  let properPerms =
        setOwnerReadable True
          $ setOwnerWritable True
          $ setOwnerExecutable True emptyPermissions
  setPermissions toPath properPerms

extractNixArtifact :: (MonadIO m, MonadError Issue m) => PkgName -> FilePath -> m ()
extractNixArtifact pkgName dir = forNixLink dir $ \link -> do
  let bin = link </> "bin" </> toString pkgName
  exists <- liftIO $ doesFileExist bin
  if exists
    then copyBinary bin (dir </> toString pkgName)
    else throwError $ fromString $ "Nix build succeeded, but binary '" <> bin <> "' not found inside the Nix store path.\n"

toNixEnv :: Text -> Text
toNixEnv name = ".#" <> toCamelCase name

inNixDevelop :: Name -> Bool -> Exec m -> Exec m
inNixDevelop envName True (Exec cmd ops env post) = Exec "nix" (["develop", toNixEnv envName, "--command", cmd] <> ops) env post
inNixDevelop _ False e = e

nixScope :: (Format p) => p -> TargetScope -> [Text]
nixScope envName ScopeGlobal =
  let envSuffix = toCamelCase (format envName)
   in [".#env-" <> envSuffix <> "-all"]
nixScope envName (ScopePkgs pkgs) =
  let envSuffix = toCamelCase (format envName)
   in map (\pkg -> ".#" <> format (pkgName pkg) <> "-" <> envSuffix) pkgs

newtype NixEnv = NixEnv {envName :: Name}

nixBuild :: (Applicative m) => NixEnv -> TargetScope -> m (Exec m)
nixBuild ctx scope = mkExec "nix" $ ["build", "--no-link"] <> nixScope (envName ctx) scope

getNixtScope :: (MonadError Issue m, MonadIO m) => TargetScope -> m PkgName
getNixtScope (ScopePkgs [pkg]) = pure $ pkgName pkg
getNixtScope _ = throwError "BuildArtifact command with Nix builder is only supported for a single package"

buildNixArtifact :: NixEnv -> FilePath -> PkgName -> m () -> Exec m
buildNixArtifact NixEnv {..} dirPath scope m = Exec "nix" ["build", ".#" <> format scope <> "-" <> format envName <> "-release", "-o", format (dirPath </> "result")] [] (Just m)

genNixBinary :: (MonadIO m, MonadError Issue m, MonadError Issue m) => NixEnv -> TargetScope -> FilePath -> m (Exec m)
genNixBinary ctx scope dirPath = do
  pkgName <- getNixtScope scope
  pure $ buildNixArtifact ctx dirPath pkgName (extractNixArtifact pkgName dirPath)