module Jupyter.Install.Internal where
import Control.Exception (Exception, IOException, catch, throwIO)
import Control.Monad (void, unless, when, foldM)
import Data.Maybe (isJust)
import System.Environment (getExecutablePath)
import System.IO (withFile, IOMode(..))
import Text.Read (readMaybe)
import Data.Typeable (Typeable)
#if !MIN_VERSION_base(4, 8, 0)
import Control.Applicative ((<$>), (<*>), pure)
import Data.Monoid (mempty)
#endif
import System.Directory (findExecutable, getTemporaryDirectory, removeDirectoryRecursive,
createDirectoryIfMissing, copyFile, doesDirectoryExist,
canonicalizePath, doesFileExist)
import System.Process (readProcess)
import qualified Data.HashMap.Lazy as HashMap
import Data.Map (Map)
import qualified Data.Map as Map
import Data.Aeson ((.=), object, encode, eitherDecode, FromJSON(..), Value(..), (.:))
import Data.Aeson.Types (Parser)
import qualified Data.ByteString.Lazy as LBS
import qualified Data.ByteString.Lazy.Char8 as CBS
import Data.Text (Text)
import qualified Data.Text as T
data Kernelspec =
Kernelspec
{ kernelspecDisplayName :: Text
, kernelspecLanguage :: Text
, kernelspecCommand :: FilePath -> FilePath -> [String]
, kernelspecJsFile :: Maybe FilePath
, kernelspecLogoFile :: Maybe FilePath
, kernelspecEnv :: Map Text Text
}
data InstallResult = InstallSuccessful
| InstallFailed Text
deriving (Eq, Ord, Show)
data InstallUser = InstallLocal
| InstallGlobal
deriving (Eq, Ord, Show)
newtype JupyterKernelspecException = JupyterKernelspecException Text
deriving (Eq, Ord, Show, Typeable)
instance Exception JupyterKernelspecException
data JupyterVersion =
JupyterVersion
{ versionMajor :: Int
, versionMinor :: Int
, versionPatch :: Int
}
deriving (Eq, Ord, Show)
showVersion :: JupyterVersion -> String
showVersion (JupyterVersion major minor patch) =
concat [show major, ".", show minor, ".", show patch]
installKernel :: InstallUser
-> Kernelspec
-> IO InstallResult
installKernel installUser kernelspec = tryInstall `catch` handleInstallFailure
where
tryInstall :: IO InstallResult
tryInstall = do
jupyterPath <- which "jupyter"
verifyJupyterCommand jupyterPath
installKernelspec installUser jupyterPath kernelspec
return InstallSuccessful
handleInstallFailure :: JupyterKernelspecException -> IO InstallResult
handleInstallFailure (JupyterKernelspecException message) = return $ InstallFailed message
installFailed :: String -> IO a
installFailed = throwIO . JupyterKernelspecException . T.pack
which :: FilePath -> IO FilePath
which cmd = do
mPath <- findExecutable cmd
case mPath of
Just path -> canonicalizePath path
Nothing ->
installFailed $ "Could not find '" ++
cmd ++
"' command on system PATH; please install it."
verifyJupyterCommand :: FilePath -> IO ()
verifyJupyterCommand jupyterPath = do
versionInfo <- runJupyterCommand jupyterPath ["--version"]
case parseVersion versionInfo of
Nothing -> installFailed $ "Could not parse output of 'jupyter --version': " ++ versionInfo
Just jupyterVersion ->
unless (jupyterVersionSupported jupyterVersion) $
installFailed $
"Invalid Jupyter version: Jupyter version 3.0 or higher required, found "
++ showVersion jupyterVersion
runJupyterCommand :: FilePath -> [String] -> IO String
runJupyterCommand jupyterPath args = readProcess jupyterPath args "" `catch` handler
where
handler :: IOException -> IO String
handler _ =
installFailed $
concat
[ "Could not run '"
, jupyterPath
, " "
, unwords args
, "'. "
, "Please make sure Jupyter is installed and functional."
]
jupyterVersionSupported :: JupyterVersion -> Bool
jupyterVersionSupported JupyterVersion{..} = versionMajor >= 3
prepareKernelspecDirectory :: Kernelspec -> FilePath -> IO ()
prepareKernelspecDirectory kernelspec dir = do
exists <- doesDirectoryExist dir
when exists $ removeDirectoryRecursive dir
createDirectoryIfMissing True dir
copyKernelspecFiles kernelspec
generateKernelJSON kernelspec
where
copyKernelspecFiles :: Kernelspec -> IO ()
copyKernelspecFiles Kernelspec { .. } = do
whenJust kernelspecJsFile $ \file -> copyFile file $ dir ++ "/kernel.js"
whenJust kernelspecLogoFile $ \file -> copyFile file $ dir ++ "/logo-64x64.png"
generateKernelJSON :: Kernelspec -> IO ()
generateKernelJSON Kernelspec { .. } = do
exePath <- getExecutablePath
withFile (dir ++ "/kernel.json") WriteMode $
flip LBS.hPutStr $
encode $
object
[ "argv" .= kernelspecCommand exePath "{connection_file}"
, "display_name" .= kernelspecDisplayName
, "language" .= kernelspecLanguage
, "env" .= kernelspecEnv
]
whenJust :: Maybe a -> (a -> IO ()) -> IO ()
whenJust Nothing _ = return ()
whenJust (Just a) f = f a
installKernelspec :: InstallUser
-> FilePath
-> Kernelspec
-> IO ()
installKernelspec installUser jupyterPath kernelspec = do
tempDir <- getTemporaryDirectory
let kernelspecDir = tempDir ++ "/" ++ T.unpack (kernelspecLanguage kernelspec)
prepareKernelspecDirectory kernelspec kernelspecDir
let userFlag =
case installUser of
InstallLocal -> ["--user"]
InstallGlobal -> []
cmd = "kernelspec" : "install" : kernelspecDir : "--replace" : userFlag
void $ runJupyterCommand jupyterPath cmd
parseVersion :: String -> Maybe JupyterVersion
parseVersion versionStr =
let versions = map (readMaybe . T.unpack) $ T.splitOn "." $ T.pack versionStr
parsed = all isJust versions
in if parsed
then case versions of
[x, y, z] -> JupyterVersion <$> x <*> y <*> z
[x, y] -> JupyterVersion <$> x <*> y <*> pure 0
[x] -> JupyterVersion <$> x <*> pure 0 <*> pure 0
_ -> Nothing
else Nothing
findKernel :: Text -> IO (Maybe Kernelspec)
findKernel language = do
Kernelspecs kernelspecs <- findKernelsInternal
maybe (return Nothing)
(fmap Just . checkKernelspecFiles)
(Map.lookup language kernelspecs)
findKernels :: IO [Kernelspec]
findKernels = do
Kernelspecs kernelspecs <- findKernelsInternal
mapM checkKernelspecFiles $ Map.elems kernelspecs
findKernelsInternal :: IO Kernelspecs
findKernelsInternal = do
jupyterPath <- which "jupyter"
specsE <- eitherDecode . CBS.pack <$> runJupyterCommand jupyterPath
["kernelspec", "list", "--json"]
case specsE of
Left err -> throwIO $ JupyterKernelspecException $ T.pack err
Right specs -> return specs
checkKernelspecFiles :: Kernelspec -> IO Kernelspec
checkKernelspecFiles spec = do
let jsFile = kernelspecJsFile spec
logoFile = kernelspecLogoFile spec
kernelspecJsFile' <- checkFile jsFile
kernelspecLogoFile' <- checkFile logoFile
return spec { kernelspecJsFile = kernelspecJsFile', kernelspecLogoFile = kernelspecLogoFile' }
where
checkFile :: Maybe FilePath -> IO (Maybe FilePath)
checkFile Nothing = return Nothing
checkFile (Just file) = do
exists <- doesFileExist file
return $ if exists
then Just file
else Nothing
newtype Kernelspecs = Kernelspecs (Map Text Kernelspec)
instance FromJSON Kernelspecs where
parseJSON (Object outer) = do
inner <- outer .: "kernelspecs"
case inner of
Object innerObj ->
let items = HashMap.toList innerObj
in Kernelspecs <$> foldM accumKernelspecs mempty items
_ -> fail "Expecting object inside 'kernelspecs' key"
parseJSON _ = fail "Expecting object with 'kernelspecs' key"
accumKernelspecs :: Map Text Kernelspec
-> (Text, Value)
-> Parser (Map Text Kernelspec)
accumKernelspecs prev (name, val) = do
kernelspec <- parseKernelspec val
return $ Map.insert name kernelspec prev
parseKernelspec :: Value -> Parser Kernelspec
parseKernelspec v =
case v of
Object o -> do
dir <- o .: "resource_dir"
spec <- o .: "spec"
Kernelspec <$> spec .: "display_name"
<*> spec .: "language"
<*> (createCommand <$> spec .: "argv")
<*> pure (Just $ dir ++ "/kernel.js")
<*> pure (Just $ dir ++ "/logo-64x64.png")
<*> spec .: "env"
_ -> fail "Expecting object for kernelspec"
where
createCommand :: [Text] -> FilePath -> FilePath -> [String]
createCommand argv _ connFile =
flip map argv $ \val ->
case val of
"{connection_file}" -> connFile
_ -> T.unpack val