elm-get-0.1.1.3: src/Get/Install.hs
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
module Get.Install (install, installAll) where
import Control.Applicative ((<$>))
import Control.Monad.Error
import Control.Monad.Writer
import qualified Data.ByteString.Lazy as BS
import qualified Data.Map as Map
import qualified Data.Maybe as Maybe
import System.Directory
import System.FilePath
import qualified Elm.Internal.Dependencies as D
import qualified Elm.Internal.Name as N
import qualified Elm.Internal.Paths as EPath
import qualified Elm.Internal.Version as V
import Get.Dependencies (defaultDeps)
import Get.Library (Library)
import qualified Get.Library as Lib
import qualified Get.Registry as R
import qualified Utils.Commands as Cmd
import qualified Utils.Paths as Path
-- | Builds up the final transformation on the dependency file using
-- WriterT
type InstallM =
(WriterT Update -- ^ The updates that need to be run on the deps
(ErrorT String IO))
execInstallM :: InstallM a -> ErrorT String IO Update
execInstallM = fmap snd . runWriterT
-- | updates to the dependencies
type DepsMap = Map.Map N.Name V.Version
-- | Nothing means don't update the file
-- | Just f means apply f to the old dependencies and replace the user's deps file.
newtype Update = Update (Maybe (Endo DepsMap))
deriving Monoid
update :: (DepsMap -> DepsMap) -> Update
update = Update . Just . Endo
-- | External Interface
installAll :: ErrorT String IO ()
installAll = installMay Nothing
install :: Library -> ErrorT String IO ()
install = installMay . Just
data InstallFlag = Create
| NoCreate
| Unknown
deriving (Show, Read, Eq, Ord)
installMay :: Maybe Library -> ErrorT String IO ()
installMay mlib =
do (shouldCreate, deps) <- (withUnknown $ D.depsAt depsFile) `catchError` askCreate
ups <-
execInstallM $ do when (shouldCreate == Create) $ tell (update id)
libs <- toInstall deps
forM_ libs $ install1 (shouldCreate /= NoCreate) deps
liftIO $
do writeUpdates deps ups
putStrLn "Done!"
where
depsFile = EPath.dependencyFile
withUnknown = fmap ((,) Unknown)
toInstall deps = case mlib of
Nothing ->
do liftIO $ putStrLn "Installing all declared dependencies..."
return $ map (\(n, v) -> Lib.Library n (Just v)) . D.dependencies $ deps
Just l -> return [l]
askCreate _ =
do yes <-
liftIO $ do putStr createMsg
Cmd.yesOrNo
unless yes . liftIO . putStr $ didntUpdateMsg
let create = if yes then Create else NoCreate
return (create, defaultDeps)
where
createMsg =
"Your project does not have a " ++ depsFile ++ " file, which the Elm\n" ++
"compiler needs to detect dependencies. Should I create it? (y/n): "
writeUpdates :: D.Deps -> Update -> IO ()
writeUpdates deps ups = case applyUpdates deps ups of
Nothing -> return ()
Just newDeps -> BS.writeFile EPath.dependencyFile (D.prettyJSON newDeps)
applyUpdates :: D.Deps -> Update -> Maybe D.Deps
applyUpdates d up = (updateDeps . wrapAssoc) (unwrap up) d
where
updateDeps :: Functor f => ([(N.Name, V.Version)] -> f [(N.Name, V.Version)]) -> D.Deps -> f D.Deps
updateDeps upper d = case d of
D.Deps { D.dependencies = deps } ->
(\deps' -> d { D.dependencies = deps'}) <$> upper deps
wrapAssoc :: (Ord k, Functor f) => (Map.Map k v -> f (Map.Map k v)) -> [(k,v)] -> f [(k,v)]
wrapAssoc upper = fmap Map.toList . upper . Map.fromList
unwrap :: Update -> DepsMap -> Maybe DepsMap
unwrap (Update m) d =
do (Endo f) <- m
return $ f d
install1 :: Bool -> D.Deps -> Library -> InstallM ()
install1 shouldAsk oldDeps l@(Lib.Library name _) =
do finalVsn <- Cmd.inDir EPath.dependencyDirectory $
do (repo,version) <- lift $ Cmd.inDir Path.internals $ getRepo l
liftIO $ createDirectoryIfMissing True repo
Cmd.copyDir (Path.internals </> repo) (repo </> show version)
return version
when shouldAsk $ mkUpdate (Map.fromList . D.dependencies $ oldDeps) name finalVsn
Cmd.out "Success!"
mkUpdate :: DepsMap -> N.Name -> V.Version -> InstallM ()
mkUpdate oldDeps n v = case Map.lookup n oldDeps of
Just v' | v == v' -> return ()
m ->
do let (askMsg, noMsg) = case m of
Nothing -> (notInstalledAsk, didntUpdateMsg)
Just v' -> (updateAsk v' v, updateNo)
yes <- shouldI askMsg
if yes
then tell $ update $ Map.insert n v
else liftIO $ putStr noMsg
where
shouldI msg = liftIO $ do putStr msg
Cmd.yesOrNo
notInstalledAsk = "Should I add this library to your " ++ depsFile ++ " file? (y/n): "
updateAsk old new = show old ++ " is already in " ++ depsFile ++ ".\nDo you want to replace it " ++ "with version " ++ show new ++ "? (y/n): "
updateNo = "Okay, but be sure to change the version number if\nyou want to use the library you just installed."
depsFile = EPath.dependencyFile
getRepo :: Library -> ErrorT String IO (FilePath, V.Version)
getRepo l =
do let directory = N.toFilePath . Lib.lib $ l
exists <- liftIO $ doesDirectoryExist directory
(if exists then update else clone) (Lib.lib l) directory
version <- getVersion l
Cmd.inDir directory (checkout version)
return (directory, version)
where
update name directory =
do Cmd.out $ "Getting updates for repo " ++ show name
Cmd.inDir directory $ do Cmd.git ["checkout", "master"]
Cmd.git ["pull"]
return ()
clone name directory =
do Cmd.out $ "Cloning repo " ++ show name
Cmd.git [ "clone", "--progress", "https://github.com/" ++ show name ++ ".git" ]
liftIO $ renameDirectory (N.project name) directory
checkout version =
do let tag = show version
Cmd.out $ "Checking out version " ++ tag
Cmd.git [ "checkout", "tags/" ++ tag ]
{-| Check to see that the requested version number exists. In the case that no
version number is requested, use the latest tagless version number in the registry.
If the repo is not in the registry, warn the user and check on github.
-}
getVersion :: Library -> ErrorT String IO V.Version
getVersion (Lib.Library name mayVsn) =
do versions <- getVersions name
case mayVsn of
Nothing ->
case filter V.tagless versions of
[] -> errorNoTags
vs -> return $ maximum vs
Just version
| version `notElem` versions -> errorNoMatch version
| otherwise -> return version
where
getVersions :: N.Name -> ErrorT String IO [V.Version]
getVersions name =
do registryVersions <- R.versions name
case registryVersions of
Just vs -> return vs
Nothing ->
do Cmd.out $ "Warning: library " ++ show name ++ " is not registered publicly. Checking github..."
tags <- lines <$> Cmd.git [ "tag", "--list" ]
return $ Maybe.mapMaybe V.fromString tags
errorNoTags =
throwError $ unlines
[ "did not find any properly tagged releases of this library."
, "Libraries have at least one tag (like 0.1.2 or 1.0) to ensure that your build"
, "process is stable and repeatable. These tags should follow Semantic Versioning."
]
errorNoMatch version =
throwError $ "could not find version " ++ show version ++ " on github."
didntUpdateMsg :: String
didntUpdateMsg =
"Okay, but if you decide to make this library visible to the compiler later, add\n\
\the dependency to your " ++ EPath.dependencyFile ++ " file."