{- Makes standalone bundle.
 -
 - Copyright 2012-2020 Joey Hess <id@joeyh.name>
 -
 - Licensed under the GNU AGPL version 3 or higher.
 -}

{-# LANGUAGE CPP #-}
{-# LANGUAGE LambdaCase #-}

module Main where

import System.Environment (getArgs)
import Control.Monad.IfElse
import System.FilePath
import System.Posix.Files
import Control.Monad
import qualified Data.ByteString.Lazy as L
import qualified Data.Map as M

import Utility.SafeCommand
import Utility.Process
import Utility.Path
import Utility.Path.AbsRel
import Utility.Directory
import Utility.Env
import Utility.FileSystemEncoding
import Build.BundledPrograms
#ifdef darwin_HOST_OS
import System.IO
import Build.OSXMkLibs (mklibs)
import Build.Version
import Utility.Split
#else
import Build.LinuxMkLibs (mklibs)
import Utility.FileMode
#endif

progDir :: FilePath -> FilePath
#ifdef darwin_HOST_OS
progDir topdir = topdir
#else
progDir topdir = topdir </> "bin"
#endif

extraProgDir :: FilePath -> FilePath
extraProgDir topdir = topdir </> "extra"

installProg :: FilePath -> FilePath -> IO (FilePath, FilePath)
installProg dir prog = searchPath prog >>= go
  where
	go Nothing = error $ "cannot find " ++ prog ++ " in PATH"
	go (Just f) = do
		let dest = dir </> takeFileName f
		unlessM (boolSystem "install" [File f, File dest]) $
			error $ "install failed for " ++ prog
		return (dest, f)

installBundledPrograms :: FilePath -> IO (M.Map FilePath FilePath)
installBundledPrograms topdir = M.fromList . concat <$> mapM go
	[ (progDir topdir, preferredBundledPrograms)
	, (extraProgDir topdir, extraBundledPrograms)
	]
  where
	go (dir, progs) = do
		createDirectoryIfMissing True dir
		forM progs $ installProg dir

installGitLibs :: FilePath -> IO ()
installGitLibs topdir = do
	-- install git-core programs; these are run by the git command
	createDirectoryIfMissing True gitcoredestdir
	execpath <- getgitpath "exec-path"
	cfs <- dirContents execpath
	forM_ cfs $ \f -> do
		destf <- ((gitcoredestdir </>) . fromRawFilePath)
			<$> relPathDirToFile
				(toRawFilePath execpath)
				(toRawFilePath f)
		createDirectoryIfMissing True (takeDirectory destf)
		issymlink <- isSymbolicLink <$> getSymbolicLinkStatus f
		if issymlink
			then do
				-- many git-core files may symlink to eg
				-- ../../bin/git, which is located outside
				-- the git-core directory. The target of
				-- such links is installed into the progDir
				-- (if not already there), and the links
				-- repointed to it.
				--
				-- Other git-core files symlink to a file
				-- beside them in the directory. Those
				-- links can be copied as-is.
				linktarget <- readSymbolicLink f
				if takeFileName linktarget == linktarget
					then cp f destf
					else do
						let linktarget' = progDir topdir </> takeFileName linktarget
						unlessM (doesFileExist linktarget') $ do
							createDirectoryIfMissing True (takeDirectory linktarget')
							L.readFile f >>= L.writeFile linktarget'
						removeWhenExistsWith removeLink destf
						rellinktarget <- relPathDirToFile
							(toRawFilePath (takeDirectory destf))
							(toRawFilePath linktarget')
						createSymbolicLink (fromRawFilePath rellinktarget) destf
			else cp f destf
	
	-- install git's template files
	-- git does not have an option to get the path of these,
	-- but they're architecture independent files, so are located
	-- next to the --man-path, in eg /usr/share/git-core
	manpath <- getgitpath "man-path"
	let templatepath = manpath </> ".." </> "git-core" </> "templates"
	tfs <- dirContents templatepath
	forM_ tfs $ \f -> do
		destf <- ((templatedestdir </>) . fromRawFilePath)
			<$> relPathDirToFile
				(toRawFilePath templatepath)
				(toRawFilePath f)
		createDirectoryIfMissing True (takeDirectory destf)
		cp f destf
  where
	gitcoredestdir = topdir </> "git-core"
	templatedestdir = topdir </> "templates"

	getgitpath v = do
		let opt = "--" ++ v
		ls <- lines <$> readProcess "git" [opt]
		case ls of
			[] -> error $ "git " ++ opt ++ "did not output a location"
			(p:_) -> return p

cp :: FilePath -> FilePath -> IO ()
cp src dest = do
	removeWhenExistsWith removeLink dest
	unlessM (boolSystem "cp" [Param "-a", File src, File dest]) $
		error "cp failed"

installMagic :: FilePath -> IO ()
#ifdef darwin_HOST_OS
installMagic topdir = getEnv "OSX_MAGIC_FILE" >>= \case
	Nothing -> hPutStrLn stderr "OSX_MAGIC_FILE not set; not including it"
	Just f -> do
		let mdir = topdir </> "magic"
		createDirectoryIfMissing True mdir
		unlessM (boolSystem "cp" [File f, File (mdir </> "magic.mgc")]) $
			error "cp failed"
#else
installMagic topdir = do
	let mdir = topdir </> "magic"
	createDirectoryIfMissing True mdir
	unlessM (boolSystem "cp" [File "/usr/share/file/magic.mgc", File (mdir </> "magic.mgc")]) $
		error "cp failed"
#endif

installLocales :: FilePath -> IO ()
#ifdef darwin_HOST_OS
installLocales _ = return ()
#else
installLocales topdir = cp "/usr/share/i18n" (topdir </> "i18n")
#endif

installSkel :: FilePath -> FilePath -> IO ()
#ifdef darwin_HOST_OS
installSkel _topdir basedir = do
	whenM (doesDirectoryExist basedir) $
		removeDirectoryRecursive basedir
	createDirectoryIfMissing True (takeDirectory basedir)
	unlessM (boolSystem "cp" [Param "-R", File "standalone/osx/git-annex.app", File basedir]) $
		error "cp failed"
#else
installSkel topdir _basedir = do
	whenM (doesDirectoryExist topdir) $
		removeDirectoryRecursive topdir
	createDirectoryIfMissing True (takeDirectory topdir)
	unlessM (boolSystem "cp" [Param "-R", File "standalone/linux/skel", File topdir]) $
		error "cp failed"
#endif

installSkelRest :: FilePath -> FilePath -> Bool -> IO ()
#ifdef darwin_HOST_OS
installSkelRest _topdir basedir _hwcaplibs = do
	plist <- lines <$> readFile "standalone/osx/Info.plist.template"
	version <- getVersion
	writeFile (basedir </> "Contents" </> "Info.plist")
		(unlines (map (expandversion version) plist))
  where
	expandversion v l = replace "GIT_ANNEX_VERSION" v l
#else
installSkelRest topdir _basedir hwcaplibs = do
	runshell <- lines <$> readFile "standalone/linux/skel/runshell"
	-- GIT_ANNEX_PACKAGE_INSTALL can be set by a distributor and
	-- runshell will be modified
	gapi <- getEnv "GIT_ANNEX_PACKAGE_INSTALL"
	writeFile (topdir </> "runshell")
		(unlines (map (expandrunshell gapi) runshell))
	modifyFileMode
		(toRawFilePath (topdir </> "runshell"))
		(addModes executeModes)
  where
	expandrunshell (Just gapi) l@"GIT_ANNEX_PACKAGE_INSTALL=" = l ++ gapi
	-- This is an optimisation, that avoids the linker looking in
	-- several directories for hwcap optimised libs, when there are
	-- none.
	expandrunshell _ l@"LD_HWCAP_MASK=" = l ++ if not hwcaplibs
		then "0"
		else ""
	expandrunshell _ l = l
#endif

installGitAnnex :: FilePath -> IO ()
#ifdef darwin_HOST_OS
installGitAnnex topdir = go topdir
#else
installGitAnnex topdir = go (topdir </> "bin")
#endif
  where
	go bindir = do
		createDirectoryIfMissing True bindir
		unlessM (boolSystem "cp" [File "git-annex", File bindir]) $
			error "cp failed"
		unlessM (boolSystem "strip" [File (bindir </> "git-annex")]) $
			error "strip failed"
		createSymbolicLink "git-annex" (bindir </> "git-annex-shell")
		createSymbolicLink "git-annex" (bindir </> "git-remote-tor-annex")
		createSymbolicLink "git-annex" (bindir </> "git-remote-annex")

main :: IO ()
main = getArgs >>= go
  where
	go (topdir:basedir:[]) = do
		installSkel topdir basedir
		installGitAnnex topdir
		installedbins <- installBundledPrograms topdir
		installGitLibs topdir
		installMagic topdir
		installLocales topdir
		hwcaplibs <- mklibs topdir installedbins
		installSkelRest topdir basedir hwcaplibs
	go _ = error "specify topdir and basedir"