{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
module Futhark.CLI.Pkg (main) where
import Control.Monad.IO.Class
import Control.Monad.State
import Control.Monad.Reader
import Data.Maybe
import qualified Data.Map as M
import qualified Data.Text as T
import qualified Data.Text.IO as T
import qualified Data.ByteString.Lazy as LBS
import Data.List
import Data.Monoid
import System.Directory
import System.FilePath
import qualified System.FilePath.Posix as Posix
import System.Environment
import System.Exit
import System.IO
import System.Console.GetOpt
import qualified Codec.Archive.Zip as Zip
import Network.HTTP.Client
import Network.HTTP.Client.TLS
import Prelude
import Futhark.Util.Options
import Futhark.Pkg.Types
import Futhark.Pkg.Info
import Futhark.Pkg.Solve
import Futhark.Util (directoryContents)
import Futhark.Util.Log
installInDir :: BuildList -> FilePath -> PkgM ()
installInDir (BuildList bl) dir = do
let putEntry from_dir pdir entry
| not (isInPkgDir from_dir $ Zip.eRelativePath entry)
|| hasTrailingPathSeparator (Zip.eRelativePath entry) = return ()
| otherwise = do
when (".." `elem` Posix.splitPath (Zip.eRelativePath entry)) $
fail $ "Zip archive for " <> pdir <> " contains suspicious path: " <>
Zip.eRelativePath entry
let f = pdir </> makeRelative from_dir (Zip.eRelativePath entry)
createDirectoryIfMissing True $ takeDirectory f
LBS.writeFile f $ Zip.fromEntry entry
isInPkgDir from_dir f =
Posix.splitPath from_dir `isPrefixOf` Posix.splitPath f
forM_ (M.toList bl) $ \(p, v) -> do
info <- lookupPackageRev p v
a <- downloadZipball $ pkgRevZipballUrl info
m <- getManifest $ pkgRevGetManifest info
let noPkgDir = fail $ "futhark.pkg for " ++ T.unpack p ++ "-" ++
T.unpack (prettySemVer v) ++ " does not define a package path."
from_dir <- maybe noPkgDir (return . (pkgRevZipballDir info <>)) $ pkgDir m
let pdir = dir </> T.unpack p
liftIO $ removePathForcibly pdir
liftIO $ createDirectoryIfMissing True pdir
liftIO $ mapM_ (putEntry from_dir pdir) $ Zip.zEntries a
libDir, libNewDir, libOldDir :: FilePath
(libDir, libNewDir, libOldDir) = ("lib", "lib~new", "lib~old")
installBuildList :: Maybe PkgPath -> BuildList -> PkgM ()
installBuildList p bl = do
libdir_exists <- liftIO $ doesDirectoryExist libDir
liftIO $ do removePathForcibly libNewDir
createDirectoryIfMissing False libNewDir
installInDir bl libNewDir
when libdir_exists $ liftIO $ do
removePathForcibly libOldDir
renameDirectory libDir libOldDir
liftIO $ renameDirectory libNewDir libDir
case pkgPathFilePath <$> p of
Just pfp | libdir_exists -> liftIO $ do
pkgdir_exists <- doesDirectoryExist $ libOldDir </> pfp
when pkgdir_exists $ do
createDirectoryIfMissing True $ takeDirectory $ libDir </> pfp
renameDirectory (libOldDir </> pfp) (libDir </> pfp)
_ -> return ()
when libdir_exists $ liftIO $ removePathForcibly libOldDir
getPkgManifest :: PkgM PkgManifest
getPkgManifest = do
file_exists <- liftIO $ doesFileExist futharkPkg
dir_exists <- liftIO $ doesDirectoryExist futharkPkg
case (file_exists, dir_exists) of
(True, _) -> liftIO $ parsePkgManifestFromFile futharkPkg
(_, True) -> fail $ futharkPkg <>
" exists, but it is a directory! What in Odin's beard..."
_ -> liftIO $ do T.putStrLn $ T.pack futharkPkg <> " not found - pretending it's empty."
return $ newPkgManifest Nothing
putPkgManifest :: PkgManifest -> PkgM ()
putPkgManifest = liftIO . T.writeFile futharkPkg . prettyPkgManifest
newtype PkgConfig = PkgConfig { pkgVerbose :: Bool }
newtype PkgM a = PkgM { unPkgM :: ReaderT PkgConfig (StateT (PkgRegistry PkgM) IO) a }
deriving (Functor, Applicative, MonadIO, MonadReader PkgConfig)
instance Monad PkgM where
PkgM m >>= f = PkgM $ m >>= unPkgM . f
return = PkgM . return
fail s = liftIO $ do
prog <- getProgName
putStrLn $ prog ++ ": " ++ s
exitFailure
instance MonadPkgRegistry PkgM where
putPkgRegistry = PkgM . put
getPkgRegistry = PkgM get
instance MonadLogger PkgM where
addLog l = do
verbose <- asks pkgVerbose
when verbose $ liftIO $ T.hPutStr stderr $ toText l
runPkgM :: PkgConfig -> PkgM a -> IO a
runPkgM cfg (PkgM m) = evalStateT (runReaderT m cfg) mempty
cmdMain :: String -> ([String] -> PkgConfig -> Maybe (IO ()))
-> String -> [String] -> IO ()
cmdMain = mainWithOptions (PkgConfig False) options
where options = [ Option "v" ["verbose"]
(NoArg $ Right $ \cfg -> cfg { pkgVerbose = True })
"Write running diagnostics to stderr."]
doFmt :: String -> [String] -> IO ()
doFmt = mainWithOptions () [] "" $ \args () ->
case args of
[] -> Just $ do
m <- parsePkgManifestFromFile futharkPkg
T.writeFile futharkPkg $ prettyPkgManifest m
_ -> Nothing
doCheck :: String -> [String] -> IO ()
doCheck = cmdMain "check" $ \args cfg ->
case args of
[] -> Just $ runPkgM cfg $ do
m <- getPkgManifest
bl <- solveDeps $ pkgRevDeps m
liftIO $ T.putStrLn "Dependencies chosen:"
liftIO $ T.putStr $ prettyBuildList bl
case commented $ manifestPkgPath m of
Nothing -> return ()
Just p -> do
let pdir = "lib" </> T.unpack p
pdir_exists <- liftIO $ doesDirectoryExist pdir
unless pdir_exists $ liftIO $ do
T.putStrLn $ "Problem: the directory " <> T.pack pdir <> " does not exist."
exitFailure
anything <- liftIO $ any ((==".fut") . takeExtension) <$>
directoryContents ("lib" </> T.unpack p)
unless anything $ liftIO $ do
T.putStrLn $ "Problem: the directory " <> T.pack pdir <> " does not contain any .fut files."
exitFailure
_ -> Nothing
doSync :: String -> [String] -> IO ()
doSync = cmdMain "" $ \args cfg ->
case args of
[] -> Just $ runPkgM cfg $ do
m <- getPkgManifest
bl <- solveDeps $ pkgRevDeps m
installBuildList (commented $ manifestPkgPath m) bl
_ -> Nothing
doAdd :: String -> [String] -> IO ()
doAdd = cmdMain "PKGPATH" $ \args cfg ->
case args of
[p, v] | Right v' <- parseVersion $ T.pack v -> Just $ runPkgM cfg $ doAdd' (T.pack p) v'
[p] -> Just $ runPkgM cfg $
doAdd' (T.pack p) =<< lookupNewestRev (T.pack p)
_ -> Nothing
where
doAdd' p v = do
m <- getPkgManifest
_ <- solveDeps $ PkgRevDeps (M.singleton p (v, Nothing)) <> pkgRevDeps m
p_info <- lookupPackageRev p v
let hash = case (_svMajor v, _svMinor v, _svPatch v) of
(0, 0, 0) -> Nothing
_ -> Just $ pkgRevCommit p_info
req = Required p v hash
(m', prev_r) = addRequiredToManifest req m
case prev_r of
Just prev_r'
| requiredPkgRev prev_r' == v ->
liftIO $ T.putStrLn $ "Package already at version " <> prettySemVer v <> "; nothing to do."
| otherwise ->
liftIO $ T.putStrLn $ "Replaced " <> p <> " " <>
prettySemVer (requiredPkgRev prev_r') <> " => " <> prettySemVer v <> "."
Nothing ->
liftIO $ T.putStrLn $ "Added new required package " <> p <> " " <> prettySemVer v <> "."
putPkgManifest m'
liftIO $ T.putStrLn "Remember to run 'futhark-pkg sync'."
doRemove :: String -> [String] -> IO ()
doRemove = cmdMain "PKGPATH" $ \args cfg ->
case args of
[p] -> Just $ runPkgM cfg $ doRemove' $ T.pack p
_ -> Nothing
where
doRemove' p = do
m <- getPkgManifest
case removeRequiredFromManifest p m of
Nothing -> liftIO $ do
T.putStrLn $ "No package " <> p <> " found in " <> T.pack futharkPkg <> "."
exitFailure
Just (m', r) -> do
putPkgManifest m'
liftIO $ T.putStrLn $ "Removed " <> p <> " " <> prettySemVer (requiredPkgRev r) <> "."
doInit :: String -> [String] -> IO ()
doInit = cmdMain "PKGPATH" $ \args cfg ->
case args of
[p] -> Just $ runPkgM cfg $ doCreate' $ T.pack p
_ -> Nothing
where
doCreate' p = do
exists <- liftIO $ (||) <$> doesFileExist futharkPkg <*> doesDirectoryExist futharkPkg
when exists $ liftIO $ do
T.putStrLn $ T.pack futharkPkg <> " already exists."
exitFailure
liftIO $ createDirectoryIfMissing True $ "lib" </> T.unpack p
liftIO $ T.putStrLn $ "Created directory " <> T.pack ("lib" </> T.unpack p) <> "."
putPkgManifest $ newPkgManifest $ Just p
liftIO $ T.putStrLn $ "Wrote " <> T.pack futharkPkg <> "."
doUpgrade :: String -> [String] -> IO ()
doUpgrade = cmdMain "" $ \args cfg ->
case args of
[] -> Just $ runPkgM cfg $ do
m <- getPkgManifest
rs <- traverse (mapM (traverse upgrade)) $ manifestRequire m
putPkgManifest m { manifestRequire = rs }
_ -> Nothing
where upgrade req = do
v <- lookupNewestRev $ requiredPkg req
h <- pkgRevCommit <$> lookupPackageRev (requiredPkg req) v
when (v /= requiredPkgRev req) $
liftIO $ T.putStrLn $ "Upgraded " <> requiredPkg req <> " " <>
prettySemVer (requiredPkgRev req) <> " => " <> prettySemVer v <> "."
return req { requiredPkgRev = v
, requiredHash = Just h }
doVersions :: String -> [String] -> IO ()
doVersions = cmdMain "PKGPATH" $ \args cfg ->
case args of
[p] -> Just $ runPkgM cfg $ doVersions' $ T.pack p
_ -> Nothing
where doVersions' =
mapM_ (liftIO . T.putStrLn . prettySemVer) . M.keys . pkgVersions
<=< lookupPackage
main :: String -> [String] -> IO ()
main prog args = do
setGlobalManager =<< newManager tlsManagerSettings
liftIO $ setEnv "GIT_TERMINAL_PROMPT" "0"
let commands = [ ("add",
(doAdd, "Add another required package to futhark.pkg."))
, ("check",
(doCheck, "Check that futhark.pkg is satisfiable."))
, ("init",
(doInit, "Create a new futhark.pkg and a lib/ skeleton."))
, ("fmt",
(doFmt, "Reformat futhark.pkg."))
, ("sync",
(doSync, "Populate lib/ as specified by futhark.pkg."))
, ("remove",
(doRemove, "Remove a required package from futhark.pkg."))
, ("upgrade",
(doUpgrade, "Upgrade all packages to newest versions."))
, ("versions",
(doVersions, "List available versions for a package."))
]
usage = "options... <" <> intercalate "|" (map fst commands) <> ">"
case args of
cmd : args' | Just (m, _) <- lookup cmd commands ->
m (unwords [prog, cmd]) args'
_ -> do
let bad _ () = Just $ do
let k = maximum (map (length . fst) commands) + 3
usageMsg $ T.unlines $
["<command> ...:", "", "Commands:"] ++
[ " " <> T.pack cmd <> T.pack (replicate (k - length cmd) ' ') <> desc
| (cmd, (_, desc)) <- commands ]
mainWithOptions () [] usage bad prog args
where usageMsg s = do
T.putStrLn $ "Usage: " <> T.pack prog <> " [--version] [--help] " <> s
exitFailure