{-|
Module:    Distribution.AppImage
Copyright: 2020 Gabriele Sales

This module provides a custom build hook for automating the creation of AppImage
bundles.

Internally, it calls the [appimagetool](https://github.com/AppImage/AppImageKit)
and [linuxdeploy](https://github.com/linuxdeploy/linuxdeploy) utilities which
must be already present on the system.
-}

{-# LANGUAGE RecordWildCards #-}

module Distribution.AppImage
  ( AppImage(..)
  , AppDirCustomize
  , appImageBuildHook
  )
where

import           Control.Monad
import           Data.Maybe
import           Data.String
import           Distribution.PackageDescription
import           Distribution.Simple
import           Distribution.Simple.LocalBuildInfo
import           Distribution.Simple.Program
import           Distribution.Simple.Program.Types
import           Distribution.Simple.Setup
import           Distribution.Simple.Utils
import           Distribution.System
import           Distribution.Verbosity
import           System.FilePath


data AppImage = AppImage {
  -- | Application name. The AppImage bundle will be produced in
  -- @dist\/build\//appName/.AppImage@ and will contain the executable
  -- /appName/.
  appName         :: String,
  -- | Path to desktop file.
  appDesktop      :: FilePath,
  -- | Application icons.
  appIcons        :: [FilePath],
  -- | Other resources to bundle. Stored in the @\usr\/share\//appName/@
  -- directory inside the image.
  appResources    :: [FilePath],
  -- | Hook to customize the generated @AppDir@ before final packaging.
  appDirCustomize :: Maybe AppDirCustomize
  }

type AppDirCustomize
  = FilePath   -- ^ AppDir path.
 -> Args       -- ^ Other parameters as defined in 'Distribution.Simple.postBuild'.
 -> BuildFlags
 -> PackageDescription
 -> LocalBuildInfo
 -> IO ()


-- | Hook for building AppImage bundles. Does nothing if the OS is not Linux.
--
-- Use this function as a @postBuild@ hook.
appImageBuildHook
  :: [AppImage] -- ^ Applications to build.
  -> Args       -- ^ Other parameters as defined in 'Distribution.Simple.postBuild'.
  -> BuildFlags
  -> PackageDescription
  -> LocalBuildInfo
  -> IO ()
appImageBuildHook apps args flags pkg buildInfo =
  when (buildOS == Linux) $
    mapM_ (makeBundle args flags pkg buildInfo) apps

makeBundle :: Args -> BuildFlags -> PackageDescription -> LocalBuildInfo -> AppImage -> IO ()
makeBundle args flags pkg buildInfo app@AppImage{..} = do
  let bdir = buildDir buildInfo
      verb = fromFlagOrDefault normal (buildVerbosity flags)
  unless (hasExecutable pkg appName) $
    die' verb ("No executable defined for the AppImage bundle: " ++ appName)
  when (null appIcons) $
    die' verb ("No icon defined for the AppImage bundle: " ++ appName)
  withTempDirectory verb bdir "appimage." $ \appDir -> do
    deployExe (bdir </> appName </> appName) app appDir verb
    bundleFiles appResources (appDir </> "usr" </> "share" </> appName) verb
    fromMaybe noCustomization appDirCustomize appDir args flags pkg buildInfo
    bundleApp appDir verb

hasExecutable :: PackageDescription -> String -> Bool
hasExecutable pkg name =
  any (\e -> exeName e == fromString name) (executables pkg)

deployExe :: FilePath -> AppImage -> FilePath -> Verbosity -> IO ()
deployExe exe AppImage{..} appDir verb = do
  prog <- findProg "linuxdeploy" verb
  runProgram verb prog $
    [ "--appdir=" ++ appDir
    , "--executable=" ++ exe
    , "--desktop-file=" ++ appDesktop ] ++
    map ("--icon-file=" ++) appIcons

bundleFiles :: [FilePath] -> FilePath -> Verbosity -> IO ()
bundleFiles files dest verb = prepare >> mapM_ copy files
  where
    prepare = createDirectoryIfMissingVerbose verb True dest

    copy file = copyFileVerbose verb file (dest </> takeFileName file)

bundleApp :: FilePath -> Verbosity -> IO ()
bundleApp appDir verb = do
  prog <- findProg "appimagetool" verb
  let (wdir, name) = splitFileName appDir
  runProgramInvocation verb $
    (programInvocation prog [name]) { progInvokeCwd = Just wdir }

noCustomization :: AppDirCustomize
noCustomization _ _ _ _ _ = return ()


findProg :: String -> Verbosity -> IO ConfiguredProgram
findProg name verb = do
  found <- findProgramOnSearchPath verb defaultProgramSearchPath name
  case found of
    Nothing        -> die' verb ("Command " ++ name ++ " is not available")
    Just (path, _) -> return (simpleConfiguredProgram name (FoundOnSystem path))