packages feed

keel-dyn-0.1.0.0: src/Keel/Dyn/Locate.hs

-- | Locate-and-load policy for native capabilities.
--
-- 'locateLibrary' documents and implements keel's search order:
--
-- 1. __Environment override__ ('specEnvVar'). If the variable is set and
--    non-empty it is authoritative: a file path is loaded exactly as
--    given, a directory is searched for 'specCandidates'. Failure of an
--    explicit override is an error — it deliberately does /not/ fall
--    through to the later stages, because silent fallback past a value
--    the user set by hand is undiagnosable.
-- 2. __Per-user data directory__ ('keelNativeDir'):
--    @~\/.local\/share\/keel\/native\/\<name\>@ per the XDG spec, or
--    @%APPDATA%\\keel\\native\\\<name\>@ on Windows. This is where
--    @keel setup@ installs checksum-pinned libraries. On Windows the
--    directory is also registered via 'addSearchDir' before loading, so
--    the library's transitive DLL dependencies resolve from it. A
--    candidate that exists here but fails to load is an error, not a
--    fallthrough, for the same reason as above.
-- 3. __System search__: each candidate is tried as a bare name through
--    the operating system's default lookup (PATH \/ @ld.so@ cache \/
--    @DYLD_*@ \/ System32).
module Keel.Dyn.Locate
  ( LibrarySpec (..)
  , Located (..)
  , Origin (..)
  , locateLibrary
  , keelNativeDir
  ) where

import Control.Monad (filterM)
import System.Directory
  ( XdgDirectory (XdgData)
  , doesDirectoryExist
  , doesFileExist
  , getXdgDirectory
  )
import System.Environment (lookupEnv)
import System.FilePath ((</>))

import Keel.Dyn.Platform

-- | What to look for and where the user may override it.
data LibrarySpec = LibrarySpec
  { specName :: String
    -- ^ Capability name, e.g. @\"openblas\"@. Names the subdirectory of
    -- the keel data dir and appears in error messages.
  , specEnvVar :: String
    -- ^ Override variable, e.g. @\"KEEL_OPENBLAS\"@. May point at a
    -- library file or at a directory containing one of the candidates.
  , specCandidates :: [FilePath]
    -- ^ Platform-appropriate file names tried in order, e.g.
    -- @[\"libopenblas.dll\"]@ \/ @[\"libopenblas.so.0\", \"libopenblas.so\"]@.
    -- The caller chooses per platform ('System.Info.os').
  }
  deriving (Eq, Show)

-- | Where a successfully located library came from — 'Keel.Doctor' level
-- diagnostics report this verbatim.
data Origin
  = FromEnvFile FilePath
    -- ^ The override variable pointed directly at this file.
  | FromEnvDir FilePath
    -- ^ Loaded from the directory the override variable pointed at.
  | FromDataDir FilePath
    -- ^ Loaded from the per-user keel data directory.
  | FromSystem FilePath
    -- ^ Resolved by the OS default search under this bare name.
  deriving (Eq, Show)

-- | A successfully located and loaded library, tagged with where the
-- search found it.
data Located = Located
  { locLibrary :: Library
  , locOrigin :: Origin
  }

-- | The per-user directory for one capability's native libraries:
-- XDG data dir (Windows: @%APPDATA%@) @\/keel\/native\/\<name\>@.
-- Derived at run time; never assumed to exist.
keelNativeDir :: String -> IO FilePath
keelNativeDir name = getXdgDirectory XdgData ("keel" </> "native" </> name)

-- | Locate and load a native library following the search order in the
-- module header. On success the 'Origin' says which stage matched.
locateLibrary :: LibrarySpec -> IO (Either DynError Located)
locateLibrary spec = do
  mOverride <- lookupEnv (specEnvVar spec)
  case mOverride of
    Just v | not (null v) -> fromEnv spec v
    _ -> do
      dir <- keelNativeDir (specName spec)
      fromDataDir spec dir

-- The override is authoritative: no fallthrough on failure.
fromEnv :: LibrarySpec -> FilePath -> IO (Either DynError Located)
fromEnv spec v = do
  isDir <- doesDirectoryExist v
  if isDir
    then do
      found <- firstExistingIn v (specCandidates spec)
      case found of
        Nothing ->
          pure . Left . LibraryNotFound v $
            specEnvVar spec
              <> " is a directory containing none of "
              <> show (specCandidates spec)
        Just path -> do
          _ <- addSearchDir v
          fmap (\lib -> Located lib (FromEnvDir path)) <$> loadLibrary path
    else fmap (\lib -> Located lib (FromEnvFile v)) <$> loadLibrary v

-- A candidate present in the data dir must load; only absence falls
-- through to the system search.
fromDataDir :: LibrarySpec -> FilePath -> IO (Either DynError Located)
fromDataDir spec dir = do
  found <- firstExistingIn dir (specCandidates spec)
  case found of
    Just path -> do
      _ <- addSearchDir dir
      fmap (\lib -> Located lib (FromDataDir path)) <$> loadLibrary path
    Nothing -> fromSystem spec (specCandidates spec)

fromSystem :: LibrarySpec -> [FilePath] -> IO (Either DynError Located)
fromSystem spec [] =
  pure . Left . LibraryNotFound (specName spec) $
    "none of "
      <> show (specCandidates spec)
      <> " found via "
      <> specEnvVar spec
      <> ", the keel data dir, or the system search path"
fromSystem spec (c : cs) = do
  r <- loadLibrary c
  case r of
    Right lib -> pure (Right (Located lib (FromSystem c)))
    Left _ -> fromSystem spec cs

firstExistingIn :: FilePath -> [FilePath] -> IO (Maybe FilePath)
firstExistingIn dir names = do
  hits <- filterM doesFileExist (map (dir </>) names)
  pure $ case hits of
    (h : _) -> Just h
    [] -> Nothing