packages feed

elm-get-0.1: src/Get/Install.hs

module Get.Install (install) where

import Control.Applicative ((<$>))
import Control.Monad (zipWithM_, when)
import Control.Monad.Error
import Data.Function (on)
import qualified Data.List as List
import qualified Data.Maybe as Maybe
import System.Directory
import System.Exit
import System.FilePath
import System.IO
import Text.JSON
import qualified Utils.PrettyJson as Pretty

import qualified Get.Registry as R
import qualified Utils.Paths as Path
import qualified Utils.Commands as Cmd
import qualified Utils.Http as Http
import qualified Elm.Internal.Dependencies as D
import qualified Elm.Internal.Paths as EPath
import qualified Elm.Internal.Name as N
import qualified Elm.Internal.Version as V

install :: N.Name -> Maybe String -> ErrorT String IO ()
install name maybeVersion =
    do version <-
           Cmd.inDir EPath.dependencyDirectory $ do
             (repo,version) <- Cmd.inDir Path.internals (get name maybeVersion)
             liftIO $ createDirectoryIfMissing True repo
             Cmd.copyDir (Path.internals </> repo) (repo </> show version)
             return version
       liftIO $ addToDepsFile name version
       Cmd.out "Success!"

get :: N.Name -> Maybe String -> ErrorT String IO (FilePath, V.Version)
get name maybeVersion =
  do exists <- liftIO $ doesDirectoryExist directory
     if exists then update else clone
     version <- getVersion name maybeVersion
     Cmd.inDir directory (checkout version)
     return (directory, version)
  where
    directory = N.toFilePath name

    update = do
      Cmd.out $ "Getting updates for repo " ++ show name
      Cmd.inDir directory $ do Cmd.git ["checkout", "master"]
                               Cmd.git ["pull"]
      return ()

    clone = 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 :: N.Name -> Maybe String -> ErrorT String IO V.Version
getVersion name maybeVersion' =
    do maybeVersion <- validateVersion maybeVersion'
       versions <- getVersions name
       case maybeVersion of
         Nothing ->
             case filter V.tagless versions of
               [] -> errorNoTags
               vs -> return $ maximum vs
         Just version
             | version `notElem` versions -> errorNoMatch version
             | otherwise                  -> return version
    where
      validateVersion :: Maybe String -> ErrorT String IO (Maybe V.Version)
      validateVersion version =
          case (version, V.fromString =<< version) of
            (Just tag, Nothing) ->
                throwError $ unlines $
                [ "tag " ++ tag ++ " is not a valid version number."
                , "It must have the following format: 0.1.2 or 0.1.2-tag"
                ]
            (_, result) -> return result

      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 $ unlines
          [ "could not find version " ++ show version ++ " on github."
          ]

addToDepsFile :: N.Name -> V.Version -> IO ()
addToDepsFile name version =
    do exists <- doesFileExist file
       add (if exists then yesFile else noFile)
    where
      file = EPath.dependencyFile

      add msg = do
        hPutStr stdout $ msg ++ " (y/n): "
        yes <- Cmd.yesOrNo
        if yes then writeFile file =<< newDependencies name version
               else hPutStrLn stdout oddChoice

      oddChoice =
          "Okay, but if you decide to make this library visible to the compiler\n\
          \later, add the dependency to your " ++ file ++ " file."

      yesFile = "Should I add this library to your " ++ file ++ " file?"
      noFile =
        concat
        [ "Your project does not have a " ++ file ++ " file yet.\n"
        , "Should I create it and add the library you just installed?" ]

newDependencies :: N.Name -> V.Version -> IO String
newDependencies name version =
    do exists <- doesFileExist EPath.dependencyFile
       raw <- if not exists then return "{}" else
                  withFile EPath.dependencyFile ReadMode $ \handle ->
                      do stuff <- hGetContents handle
                         length stuff `seq` return stuff
       case decode raw of
         Error msg -> do
           hPutStrLn stderr $ "Error reading " ++ EPath.dependencyFile ++ ":\n" ++ msg
           exitFailure
         Ok obj ->
             let assocs = fromJSObject obj in
             case List.lookup "dependencies" assocs of
               Just (JSObject entries) -> do
                 entries' <- updateEntries (fromJSObject entries)
                 return $ addDeps assocs entries'
               _ -> return $ addDeps assocs [entry]

    where
      entry = (show name, JSString $ toJSString $ show version)

      addDeps assocs entries = show $ Pretty.object obj
          where
            assocs' = filter ((/=) "dependencies" . fst) assocs
            obj = assocs' ++ [("dependencies", JSObject $ toJSObject entries)]

      updateEntries ::[(String,JSValue)] -> IO [(String,JSValue)]
      updateEntries entries =
          let name' = show name
              entries' = List.insertBy (compare `on` fst) entry $
                         filter ((/=) name' . fst) entries
          in
          case List.lookup name' entries of
            Just (JSString oldVersion) -> do
              hPutStr stdout $
                 name' ++ " " ++ fromJSString oldVersion ++ " is already in " ++
                 EPath.dependencyFile ++ ".\nDo you want to replace it " ++
                 "with version " ++ show version ++ "? (y/n): "
              yes <- Cmd.yesOrNo
              case yes of
                True -> return entries'
                False -> hPutStrLn stdout msg >> return entries
                    where msg = "Okay, but be sure to change the version number if\n\
                                \you want to use the library you just installed."

            _ -> return entries'