hwm-0.5.0: src/HWM/CLI/Command/Release/Artifacts.hs
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE NoImplicitPrelude #-}
module HWM.CLI.Command.Release.Artifacts
( ReleaseArchiveOptions (..),
parseCLI,
runReleaseArchive,
)
where
import Control.Monad.Error.Class (MonadError (throwError))
import Data.Traversable (for)
import HWM.Core.Common (Name)
import HWM.Core.Formatting (Format (format), Status (..), formatList, statusIcon)
import HWM.Core.Parsing (Parse (..), ParseCLI (..), parseLS)
import HWM.Core.Result (fromEither)
import HWM.Domain.Build (BuildFlag (GHCOptionsFlag), Builder (..), BuilderCommand (..))
import HWM.Domain.Config (Config (..))
import HWM.Domain.ConfigT (ConfigT, Env (..), getArchiveConfigs)
import HWM.Domain.Dispatcher (DispatcheCommand (..), dispatch)
import HWM.Domain.Environments (BuildEnvironment (..), getBuildEnvironment, overrideBuilder)
import HWM.Domain.Release (ArchiveFormat, ArtifactConfig (..), ReleaseArtifactConfigs, isArtifactEnabledInEnvironment, resolveArtifactConfig, selectedArtifacts)
import HWM.Domain.Schema (TargetScope (ScopePkgs))
import HWM.Integrations.Toolchain.Github (ensureIsLatestTag)
import HWM.Runtime.Archive (ArchiveInfo (..), ArchivingPlan (..), createArchive)
import HWM.Runtime.Network (getGHUploadUrl, uploadToGitHub)
import HWM.Runtime.UI (indent, section, sectionTableM, uiSpace, uiSubPath)
import Options.Applicative (argument, help, long, metavar, option, showDefault, str, strOption, switch, value)
import Relude
import System.Directory (createDirectoryIfMissing, removePathForcibly)
import System.FilePath (joinPath)
-- | Options for 'hwm release archive'
data ReleaseArchiveOptions = ReleaseArchiveOptions
{ targetName :: Maybe Text,
ghPublish :: Bool,
outputDir :: FilePath,
ovFormat :: Maybe [Text],
ovGhcOptions :: Maybe [Text],
ovNameTemplate :: Maybe Text,
opsBuilder :: Maybe Builder
}
deriving (Show)
instance ParseCLI ReleaseArchiveOptions where
parseCLI =
ReleaseArchiveOptions
<$> optional (argument str (metavar "TARGET" <> help "Name of the release target to build (default: all)"))
<*> switch (long "github" <> help "Upload generated artifacts to a GitHub Release")
<*> strOption
( long "output-dir"
<> metavar "OUTPUT_DIR"
<> help "Directory to output the release artifacts."
<> value defaultOutputDir
<> showDefault
)
<*> optional (option (parseLS <$> str) (long "format" <> metavar "FORMAT" <> help "Override the archive format for the release target. Supported: zip, tar.gz."))
<*> (mkOptional . concat <$> many (option (parseLS <$> str) (long "ghc-options" <> metavar "GHC_OPTION" <> help "Override GHC options for the release target. Repeatable; also accepts comma-separated values.")))
<*> optional (strOption (long "name-template" <> metavar "NAME_TEMPLATE" <> help "Override the name template for the release target. Use {name} and {version} as placeholders."))
<*> optional (option (str >>= parse) (long "builder" <> metavar "BUILDER" <> help "Override the builder for the release target. Supported: cabal, stack, nix."))
genBindaryDir :: (MonadIO m, ToString a) => a -> m FilePath
genBindaryDir name = do
let path = joinPath [".hwm/release/binaries", toString name]
prepeareDir path
pure path
defaultOutputDir :: FilePath
defaultOutputDir = ".hwm/dist"
prepeareDir :: (MonadIO m) => FilePath -> m ()
prepeareDir dir = liftIO $ do
removePathForcibly dir
createDirectoryIfMissing True dir
mkOptional :: [a] -> Maybe [a]
mkOptional [] = Nothing
mkOptional xs = Just xs
parseFormats :: [Text] -> ConfigT [ArchiveFormat]
parseFormats = fromEither "can't parse archive format" . traverse parse
validateTargetEnvironment :: Name -> (Name, ArtifactConfig) -> ConfigT ()
validateTargetEnvironment envName (target, cfg@ArtifactConfig {arcEnvironments = allowedEnvs}) =
unless (isArtifactEnabledInEnvironment envName cfg)
$ throwError
$ fromString
$ "Artifact '"
<> toString target
<> "' is not enabled for environment '"
<> toString envName
<> "'. Allowed environments: "
<> (if maybe True null allowedEnvs then "<none>" else toString (formatList ", " (fromMaybe [] allowedEnvs)))
withOverrides :: ReleaseArchiveOptions -> ReleaseArtifactConfigs -> ConfigT [(Name, ArtifactConfig)]
withOverrides ReleaseArchiveOptions {..} cfgs = do
parsedFormats <- traverse parseFormats ovFormat
map (second (applyOverrieds parsedFormats)) <$> selectedArtifacts targetName cfgs
where
applyOverrieds formats cfg =
cfg
{ arcFormats = fromMaybe (arcFormats cfg) formats,
arcGhcOptions = fromMaybe (arcGhcOptions cfg) ovGhcOptions,
arcNameTemplate = fromMaybe (arcNameTemplate cfg) ovNameTemplate
}
runReleaseArchive :: ReleaseArchiveOptions -> ConfigT ()
runReleaseArchive ops@ReleaseArchiveOptions {..} = do
if outputDir == defaultOutputDir
then prepeareDir defaultOutputDir
else liftIO $ createDirectoryIfMissing True outputDir
cfg <- asks config
version <- asks (cfgVersion . config)
cfgs <- getArchiveConfigs >>= withOverrides ops
ghTag <- if ghPublish then Just <$> ensureIsLatestTag version else pure Nothing
uploadUrl <- maybe (pure Nothing) (fmap Just . getGHUploadUrl cfg) ghTag
activeEnv <- getBuildEnvironment Nothing
let defaultBuilder = buildBuilder activeEnv
let builder = fromMaybe defaultBuilder opsBuilder
traverse_ (validateTargetEnvironment (buildName activeEnv)) cfgs
sectionTableM
"artifacts"
[ ("destination", pure $ maybe (format outputDir) format uploadUrl),
("version", pure $ format version <> maybe "" (\tag -> " (GitHub Release " <> tag <> ")") ghTag),
("targets", pure $ formatList "," (map fst cfgs)),
("builder", pure $ format builder)
]
plans <- section "build" $ traverse (buildPkg outputDir builder) cfgs
uiSpace
section "archive" $ pure ()
artifacts <- for plans $ \(name, plan) -> do
archives <- createArchive version plan
indent 1
$ section name
$ for_ archives
$ \ArchiveInfo {..} -> do
uiSubPath archivePath
uiSubPath sha256Path
pure (name, archives)
for_ uploadUrl $ \url -> section "publish (Github)"
$ for_ artifacts
$ \(name, archives) -> section name $ for_ archives $ \ArchiveInfo {..} -> do
uploadToGitHub url archivePath
uiSubPath archivePath
uploadToGitHub url sha256Path
uiSubPath sha256Path
buildPkg :: FilePath -> Builder -> (Name, ArtifactConfig) -> ConfigT (Text, ArchivingPlan)
buildPkg outputDir builder (name, cfg@ArtifactConfig {..}) = do
binaryDir <- genBindaryDir name
(executableName, pkg) <- resolveArtifactConfig cfg
env <- overrideBuilder builder <$> getBuildEnvironment Nothing
dispatch (DispatcheCommand (BuildArtifact binaryDir) (ScopePkgs [pkg]) (map GHCOptionsFlag arcGhcOptions)) env
pure (statusIcon Checked, ArchivingPlan {nameTemplate = arcNameTemplate, outDir = outputDir, sourceDir = binaryDir, name = executableName, archiveFormats = arcFormats})