-- | @futhark pkg@ module Futhark.CLI.Pkg (main) where import Control.Monad import Control.Monad.IO.Class import Control.Monad.Reader import Control.Monad.State import Data.List (intercalate) import Data.Map qualified as M import Data.Maybe import Data.Monoid import Data.Text qualified as T import Data.Text.IO qualified as T import Futhark.Pkg.Info import Futhark.Pkg.Solve import Futhark.Pkg.Types import Futhark.Util (directoryContents, maxinum) import Futhark.Util.Log import Futhark.Util.Options import System.Directory import System.Environment import System.Exit import System.FilePath import System.IO import System.IO.Temp (withSystemTempDirectory) import Prelude --- Installing packages installInDir :: CacheDir -> BuildList -> FilePath -> PkgM () installInDir cachedir (BuildList bl) dir = forM_ (M.toList bl) $ \(p, v) -> do info <- lookupPackageRev cachedir p v (filedir, files) <- getFiles $ pkgGetFiles info -- The directory in the local file system that will contain the -- package files. let pdir = dir T.unpack p -- Remove any existing directory for this package. This is a bit -- inefficient, as the likelihood that the old ``lib`` directory -- already contains the correct version is rather high. We should -- have a way to recognise this situation, and not download the -- zipball in that case. liftIO $ removePathForcibly pdir forM_ files $ \file -> do let from = filedir file to = pdir file liftIO $ createDirectoryIfMissing True $ takeDirectory to logMsg $ "Copying " <> from <> "\n" <> "to " <> to liftIO $ copyFile from to libDir, libNewDir, libOldDir :: FilePath (libDir, libNewDir, libOldDir) = ("lib", "lib~new", "lib~old") -- | Install the packages listed in the build list in the @lib@ -- directory of the current working directory. Since we are touching -- the file system, we are going to be very paranoid. In particular, -- we want to avoid corrupting the @lib@ directory if something fails -- along the way. -- -- The procedure is as follows: -- -- 1) Create a directory @lib~new@. Delete an existing @lib~new@ if -- necessary. -- -- 2) Populate @lib~new@ based on the build list. -- -- 3) Rename @lib@ to @lib~old@. Delete an existing @lib~old@ if -- necessary. -- -- 4) Rename @lib~new@ to @lib@ -- -- 5) If the current package has package path @p@, move @lib~old/p@ to -- @lib~new/p@. -- -- 6) Delete @lib~old@. -- -- Since POSIX at least guarantees atomic renames, the only place this -- can fail is between steps 3, 4, and 5. In that case, at least the -- @lib~old@ will still exist and can be put back by the user. installBuildList :: CacheDir -> Maybe PkgPath -> BuildList -> PkgM () installBuildList cachedir p bl = do libdir_exists <- liftIO $ doesDirectoryExist libDir -- 1 liftIO $ do removePathForcibly libNewDir createDirectoryIfMissing False libNewDir -- 2 installInDir cachedir bl libNewDir -- 3 when libdir_exists $ liftIO $ do removePathForcibly libOldDir renameDirectory libDir libOldDir -- 4 liftIO $ renameDirectory libNewDir libDir -- 5 case pkgPathFilePath <$> p of Just pfp | libdir_exists -> liftIO $ do pkgdir_exists <- doesDirectoryExist $ libOldDir pfp when pkgdir_exists $ do -- Ensure the parent directories exist so that we can move the -- package directory directly. createDirectoryIfMissing True $ takeDirectory $ libDir pfp renameDirectory (libOldDir pfp) (libDir pfp) _ -> pure () -- 6 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." pure $ newPkgManifest Nothing putPkgManifest :: PkgManifest -> PkgM () putPkgManifest = liftIO . T.writeFile futharkPkg . prettyPkgManifest --- The CLI newtype PkgConfig = PkgConfig {pkgVerbose :: Bool} -- | The monad in which futhark-pkg runs. 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 instance MonadFail PkgM where 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.hPutStrLn 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 withCacheDir :: (CacheDir -> IO a) -> IO a withCacheDir f = withSystemTempDirectory "futhark-pkg" $ f . CacheDir doCheck :: String -> [String] -> IO () doCheck = cmdMain "check" $ \args cfg -> case args of [] -> Just . withCacheDir $ \cachedir -> runPkgM cfg $ do m <- getPkgManifest bl <- solveDeps cachedir $ pkgRevDeps m liftIO $ T.putStrLn "Dependencies chosen:" liftIO $ T.putStr $ prettyBuildList bl case commented $ manifestPkgPath m of Nothing -> pure () 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 . withCacheDir $ \cachedir -> runPkgM cfg $ do m <- getPkgManifest bl <- solveDeps cachedir $ pkgRevDeps m installBuildList cachedir (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 $ withCacheDir $ \cachedir -> runPkgM cfg $ doAdd' cachedir (T.pack p) v' [p] -> Just $ withCacheDir $ \cachedir -> runPkgM cfg $ -- Look up the newest revision of the package. doAdd' cachedir (T.pack p) =<< lookupNewestRev cachedir (T.pack p) _ -> Nothing where doAdd' cachedir p v = do m <- getPkgManifest -- See if this package (and its dependencies) even exists. We -- do this by running the solver with the dependencies already -- in the manifest, plus this new one. The Monoid instance for -- PkgRevDeps is left-biased, so we are careful to use the new -- version for this package. _ <- solveDeps cachedir $ PkgRevDeps (M.singleton p (v, Nothing)) <> pkgRevDeps m -- We either replace any existing occurence of package 'p', or -- we add a new one. p_info <- lookupPackageRev cachedir p v let hash = case (_svMajor v, _svMinor v, _svPatch v) of -- We do not perform hash-pinning for -- (0,0,0)-versions, because these already embed a -- specific revision ID into their version number. (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 validPkgPath p = not $ any (`elem` [".", ".."]) $ splitDirectories $ T.unpack p doCreate' p = do unless (validPkgPath p) . liftIO $ do T.putStrLn $ "Not a valid package path: " <> p T.putStrLn "Note: package paths are usually URIs." T.putStrLn "Note: 'futhark init' is only needed when creating a package, not to use packages." exitFailure 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 . withCacheDir $ \cachedir -> runPkgM cfg $ do m <- getPkgManifest rs <- traverse (mapM (traverse (upgrade cachedir))) $ manifestRequire m putPkgManifest m {manifestRequire = rs} if rs == manifestRequire m then liftIO $ T.putStrLn "Nothing to upgrade." else liftIO $ T.putStrLn "Remember to run 'futhark pkg sync'." _ -> Nothing where upgrade cachedir req = do v <- lookupNewestRev cachedir $ requiredPkg req h <- pkgRevCommit <$> lookupPackageRev cachedir (requiredPkg req) v when (v /= requiredPkgRev req) $ liftIO $ T.putStrLn $ "Upgraded " <> requiredPkg req <> " " <> prettySemVer (requiredPkgRev req) <> " => " <> prettySemVer v <> "." pure req { requiredPkgRev = v, requiredHash = Just h } doVersions :: String -> [String] -> IO () doVersions = cmdMain "PKGPATH" $ \args cfg -> case args of [p] -> Just $ withCacheDir $ \cachedir -> runPkgM cfg $ doVersions' cachedir $ T.pack p _ -> Nothing where doVersions' cachedir = mapM_ (liftIO . T.putStrLn . prettySemVer) . M.keys . pkgVersions <=< lookupPackage cachedir -- | Run @futhark pkg@. main :: String -> [String] -> IO () main prog args = do -- Avoid Git asking for credentials. We prefer failure. 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 = maxinum (map (length . fst) commands) + 3 usageMsg . T.unlines $ [" ...:", "", "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