ihaskell-0.3.0.0: src/IHaskell/IPython.hs
{-# LANGUAGE NoImplicitPrelude, OverloadedStrings #-}
{-# LANGUAGE DoAndIfThenElse #-}
-- | Description : Shell scripting wrapper using @Shelly@ for the @notebook@, @setup@, and
-- @console@ commands.
module IHaskell.IPython (
ipythonInstalled,
installIPython,
updateIPython,
setupIPython,
runConsole,
runNotebook,
readInitInfo,
defaultConfFile,
getIHaskellDir,
getSandboxPackageConf,
nbconvert,
ViewFormat(..),
) where
import ClassyPrelude
import Prelude (read, reads, init)
import Shelly hiding (find, trace, path, (</>))
import System.Argv0
import System.Directory
import qualified Filesystem.Path.CurrentOS as FS
import Data.List.Utils (split)
import Data.String.Utils (rstrip, endswith)
import Text.Printf
import qualified System.IO.Strict as StrictIO
import qualified Paths_ihaskell as Paths
import qualified Codec.Archive.Tar as Tar
import IHaskell.Types
-- | Which commit of IPython we are on.
ipythonCommit :: Text
ipythonCommit = "9c922f54af799704f4000aeee94ec7c74cada194"
-- | The IPython profile name.
ipythonProfile :: String
ipythonProfile = "haskell"
-- | Run IPython with any arguments.
ipython :: Bool -- ^ Whether to suppress output.
-> [Text] -- ^ IPython command line arguments.
-> Sh String -- ^ IPython output.
ipython suppress args = do
ipythonPath <- ipythonExePath
runHandles ipythonPath args handles doNothing
where handles = [InHandle Inherit, outHandle suppress, errorHandle suppress]
outHandle True = OutHandle CreatePipe
outHandle False = OutHandle Inherit
errorHandle True = ErrorHandle CreatePipe
errorHandle False = ErrorHandle Inherit
doNothing _ stdout _ = if suppress
then liftIO $ StrictIO.hGetContents stdout
else return ""
-- | Run while suppressing all output.
quietRun path args = runHandles path args handles nothing
where
handles = [InHandle Inherit, OutHandle CreatePipe, ErrorHandle CreatePipe]
nothing _ _ _ = return ()
-- | Create the directory and return it.
ensure :: Sh FilePath -> Sh FilePath
ensure getDir = do
dir <- getDir
mkdir_p dir
return dir
-- | Return the data directory for IHaskell.
ihaskellDir :: Sh FilePath
ihaskellDir = do
home <- maybe (error "$HOME not defined.") fromText <$> get_env "HOME"
ensure $ return (home </> ".ihaskell")
ipythonDir :: Sh FilePath
ipythonDir = ensure $ (</> "ipython") <$> ihaskellDir
ipythonExePath :: Sh FilePath
ipythonExePath = (</> ("bin" </> "ipython")) <$> ipythonDir
notebookDir :: Sh FilePath
notebookDir = ensure $ (</> "notebooks") <$> ihaskellDir
ipythonSourceDir :: Sh FilePath
ipythonSourceDir = ensure $ (</> "ipython-src") <$> ihaskellDir
getIHaskellDir :: IO String
getIHaskellDir = shellyNoDir $ fpToString <$> ihaskellDir
defaultConfFile :: IO (Maybe String)
defaultConfFile = shellyNoDir $ do
filename <- (</> "rc.hs") <$> ihaskellDir
exists <- test_f filename
return $ if exists
then Just $ fpToString filename
else Nothing
-- | Find a notebook and then convert it into the provided format.
-- Notebooks are searched in the current directory as well as the IHaskell
-- notebook directory (in that order).
nbconvert :: ViewFormat -> String -> IO ()
nbconvert fmt name = void . shellyNoDir $ do
curdir <- pwd
nbdir <- notebookDir
-- Find which of the options is available.
let notebookOptions = [
curdir </> fpFromString name,
curdir </> fpFromString (name ++ ".ipynb"),
nbdir </> fpFromString name,
nbdir </> fpFromString (name ++ ".ipynb")
]
maybeNb <- headMay <$> filterM test_f notebookOptions
case maybeNb of
Nothing -> do
putStrLn $ "Cannot find notebook: " ++ pack name
putStrLn "Tried:"
mapM_ (putStrLn . (" " ++) . fpToText) notebookOptions
Just notebook ->
let viewArgs = case fmt of
Pdf -> ["--to=latex", "--post=pdf"]
Html -> ["--to=html", "--template=ihaskell"]
fmt -> ["--to=" ++ show fmt] in
void $ runIHaskell ipythonProfile "nbconvert" $ viewArgs ++ [fpToString notebook]
-- | Set up IPython properly.
setupIPython :: IO ()
setupIPython = do
installed <- ipythonInstalled
if installed
then updateIPython
else installIPython
-- | Update the IPython source tree and rebuild.
updateIPython :: IO ()
updateIPython = void . shellyNoDir $ do
srcDir <- ipythonSourceDir
cd srcDir
gitPath <- path "git"
currentCommitHash <- silently $ pack <$> rstrip <$> unpack <$> run gitPath ["rev-parse", "HEAD"]
when (currentCommitHash /= ipythonCommit) $ do
putStrLn "Incorrect IPython repository commit hash."
putStrLn $ "Found hash: " ++ currentCommitHash
putStrLn $ "Wanted hash: " ++ ipythonCommit
putStrLn "Updating..."
run_ gitPath ["pull", "origin", "master"]
run_ gitPath ["checkout", ipythonCommit]
installPipDependencies
buildIPython
-- | Install IPython from source.
installIPython :: IO ()
installIPython = void . shellyNoDir $ do
installPipDependencies
-- Get the IPython source.
gitPath <- path "git"
putStrLn "Downloading IPython... (this may take a while)"
ipythonSrcDir <- ipythonSourceDir
run_ gitPath ["clone", "--recursive", "https://github.com/ipython/ipython.git", fpToText ipythonSrcDir]
cd ipythonSrcDir
run_ gitPath ["checkout", ipythonCommit]
buildIPython
-- | Install all Python dependencies.
installPipDependencies :: Sh ()
installPipDependencies = withTmpDir $ \tmpDir ->
mapM_ (installDependency tmpDir)
[
("pyzmq", "14.0.1")
, ("tornado","3.1.1")
, ("jinja2","2.7.1")
-- The following cannot go first in the dependency list, because
-- their setup.py are broken and require the directory to exist
-- already.
, ("MarkupSafe", "0.18")
--, ("setuptools", "2.0.2")
]
where
installDependency :: FilePath -> (Text, Text) -> Sh ()
installDependency tmpDir (dep, version) = sub $ do
let versioned = dep ++ "-" ++ version
putStrLn $ "Installing dependency: " ++ versioned
pipPath <- path "pip"
tarPath <- path "tar"
pythonPath <- path "python"
-- Download the package.
let downloadOpt = "--download=" ++ fpToText tmpDir
run_ pipPath ["install", downloadOpt, dep ++ "==" ++ version]
-- Extract it.
cd tmpDir
run_ tarPath ["-xzf", versioned ++ ".tar.gz"]
-- Install it.
cd $ fromText versioned
dir <- fpToText <$> ipythonDir
setenv "PYTHONPATH" $ dir ++ "/lib/python2.7/site-packages/"
let prefixOpt = "--prefix=" ++ dir
run_ pythonPath ["setup.py", "install", prefixOpt]
-- | Once things are checked out into the IPython source directory, build it and install it.
buildIPython :: Sh ()
buildIPython = do
-- Install IPython locally.
pythonPath <- path "python"
prefixOpt <- ("--prefix=" ++) <$> fpToText <$> ipythonDir
putStrLn "Installing IPython."
run_ pythonPath ["setup.py", "install", prefixOpt]
-- Patch the IPython executable so that it doesn't use system IPython.
-- Using PYTHONPATH is not enough due to bugs in how `easy_install` sets
-- things up, at least on Mac OS X.
ipyDir <- ipythonDir
let patchLines =
[ "#!/usr/bin/env python"
, "import sys"
, "sys.path = [\"" ++ fpToText ipyDir ++
"/lib/python2.7/site-packages\"] + sys.path"]
ipythonPath <- ipythonExePath
contents <- readFile ipythonPath
writeFile ipythonPath $ unlines patchLines ++ "\n" ++ contents
-- Remove the old IPython profile so that we write a new one in its
-- place. Users are not expected to fiddle with the profile, so we give
-- no warning whatsoever. This may be changed eventually.
removeIPythonProfile ipythonProfile
-- | Check whether IPython is properly installed.
ipythonInstalled :: IO Bool
ipythonInstalled = shellyNoDir $ do
ipythonPath <- ipythonExePath
test_f ipythonPath
-- | Get the path to an executable. If it doensn't exist, fail with an
-- error message complaining about it.
path :: Text -> Sh FilePath
path exe = do
path <- which $ fromText exe
case path of
Nothing -> do
putStrLn $ "Could not find `" ++ exe ++ "` executable."
fail $ "`" ++ unpack exe ++ "` not on $PATH."
Just exePath -> return exePath
-- | Use the `ipython --version` command to figure out the version.
-- Return a tuple with (major, minor, patch).
ipythonVersion :: IO (Int, Int, Int)
ipythonVersion = shellyNoDir $ do
[major, minor, patch] <- parseVersion <$> ipython True ["--version"]
return (major, minor, patch)
-- | Parse an IPython version string into a list of integers.
parseVersion :: String -> [Int]
parseVersion versionStr = map read' $ split "." versionStr
where read' x = case reads x of
[(n, _)] -> n
_ -> error $ "cannot parse version: "++ versionStr
-- | Run an IHaskell application using the given profile.
runIHaskell :: String -- ^ IHaskell profile name.
-> String -- ^ IPython app name.
-> [String] -- ^ Arguments to IPython.
-> Sh ()
runIHaskell profile app args = void $ do
-- Try to locate the profile. Do not die if it doesn't exist.
errExit False $ ipython True ["locate", "profile", pack profile]
-- If the profile doesn't exist, create it.
-- We have an ugly hack that removes the profile whenever the IPython
-- version is updated. This means profiles get updated with IPython.
exitCode <- lastExitCode
when (exitCode /= 0) $ liftIO $ do
putStrLn "Creating IPython profile."
setupIPythonProfile profile
-- Run the IHaskell command.
ipython False $ map pack $ [app, "--profile", profile] ++ args
runConsole :: InitInfo -> IO ()
runConsole initInfo = void . shellyNoDir $ do
writeInitInfo initInfo
runIHaskell ipythonProfile "console" []
runNotebook :: InitInfo -> Maybe String -> IO ()
runNotebook initInfo maybeServeDir = void . shellyNoDir $ do
notebookDirStr <- fpToString <$> notebookDir
let args = case maybeServeDir of
Nothing -> ["--notebook-dir", unpack notebookDirStr]
Just dir -> ["--notebook-dir", dir]
writeInitInfo initInfo
runIHaskell ipythonProfile "notebook" args
writeInitInfo :: InitInfo -> Sh ()
writeInitInfo info = do
filename <- (</> ".last-arguments") <$> ihaskellDir
liftIO $ writeFile filename $ show info
readInitInfo :: IO InitInfo
readInitInfo = shellyNoDir $ do
filename <- (</> ".last-arguments") <$> ihaskellDir
read <$> liftIO (readFile filename)
-- | Create the IPython profile.
setupIPythonProfile :: String -- ^ IHaskell profile name.
-> IO ()
setupIPythonProfile profile = shellyNoDir $ do
-- Create the IPython profile.
void $ ipython True ["profile", "create", pack profile]
-- Find the IPython profile directory. Make sure to get rid of trailing
-- newlines from the output of the `ipython locate` call.
ipythonDir <- pack <$> rstrip <$> ipython True ["locate"]
let profileDir = ipythonDir ++ "/profile_" ++ pack profile ++ "/"
liftIO $ copyProfile profileDir
insertIHaskellPath profileDir
removeIPythonProfile :: String -> Sh ()
removeIPythonProfile profile = do
-- Try to locate the profile. Do not die if it doesn't exist.
errExit False $ ipython True ["locate", "profile", pack profile]
-- If the profile exists, delete it.
exitCode <- lastExitCode
dir <- pack <$> rstrip <$> ipython True ["locate"]
when (exitCode == 0 && dir /= "") $ do
putStrLn "Updating IPython profile."
let profileDir = dir ++ "/profile_" ++ pack profile ++ "/"
rm_rf $ fromText profileDir
-- | Copy the profile files into the IPython profile.
copyProfile :: Text -> IO ()
copyProfile profileDir = do
profileTar <- Paths.getDataFileName "profile/profile.tar"
{-
-- Load profile from Resources directory of Mac *.app.
ihaskellPath <- shellyNoDir getIHaskellPath
profileTar <- if "IHaskell.app/Contents/MacOS" `isInfixOf` ihaskellPath
then
let pieces = split "/" ihaskellPath
pathPieces = init pieces ++ ["..", "Resources", "profile.tar"] in
return $ intercalate "/" pathPieces
else Paths.getDataFileName "profile/profile.tar"
-}
putStrLn $ pack $ "Loading profile from " ++ profileTar
Tar.extract (unpack profileDir) profileTar
-- | Insert the IHaskell path into the IPython configuration.
insertIHaskellPath :: Text -> Sh ()
insertIHaskellPath profileDir = do
path <- getIHaskellPath
let filename = profileDir ++ "ipython_config.py"
template = "exe = '%s'.replace(' ', '\\\\ ')"
exeLine = printf template $ unpack path :: String
liftIO $ do
contents <- StrictIO.readFile $ unpack filename
writeFile (fromText filename) $ exeLine ++ "\n" ++ contents
-- | Get the absolute path to this IHaskell executable.
getIHaskellPath :: Sh String
getIHaskellPath = do
-- Get the absolute filepath to the argument.
f <- liftIO getArgv0
-- If we have an absolute path, that's the IHaskell we're interested in.
if FS.absolute f
then return $ FS.encodeString f
else
-- Check whether this is a relative path, or just 'IHaskell' with $PATH
-- resolution done by the shell. If it's just 'IHaskell', use the $PATH
-- variable to find where IHaskell lives.
if FS.filename f == f
then do
ihaskellPath <- which "IHaskell"
case ihaskellPath of
Nothing -> error "IHaskell not on $PATH and not referenced relative to directory."
Just path -> return $ FS.encodeString path
else do
-- If it's actually a relative path, make it absolute.
cd <- liftIO getCurrentDirectory
return $ FS.encodeString $ FS.decodeString cd FS.</> f
getSandboxPackageConf :: IO (Maybe String)
getSandboxPackageConf = shellyNoDir $ do
myPath <- getIHaskellPath
let sandboxName = ".cabal-sandbox"
if not $ sandboxName`isInfixOf` myPath
then return Nothing
else do
let pieces = split "/" myPath
sandboxDir = intercalate "/" $ takeWhile (/= sandboxName) pieces ++ [sandboxName]
subdirs <- ls $ fpFromString sandboxDir
let confdirs = filter (endswith "packages.conf.d") $ map fpToString subdirs
case confdirs of
[] -> return Nothing
dir:_ ->
return $ Just dir