keel-0.1.0.0: src/Keel/Setup.hs
-- | @keel setup@: install pinned native runtimes into the per-user
-- keel directory ("Keel.Dyn.Locate"'s second search stage).
--
-- Every artifact is an /official upstream release archive/, pinned by
-- URL and SHA-256 (values computed from the downloaded archives,
-- 2026-08-18). The tools used are deliberately boring: the system's
-- @curl@ and @tar@ (Windows 10+ ships both; the System32 bsdtar also
-- unpacks zip), plus a pure-Haskell SHA-256 — keel's zero-native-deps
-- rule applies to keel itself.
--
-- Offline\/air-gapped environments skip @keel setup@ entirely: point
-- @KEEL_OPENBLAS@ \/ @KEEL_ONNXRUNTIME@ at an existing library, or drop
-- one into the keel data dir yourself.
module Keel.Setup
( SetupError (..)
, setupBlas
, setupOnnx
) where
import Control.Exception (Exception)
import Control.Monad (filterM, forM_, when)
import Data.ByteString.Lazy qualified as BL
import Data.Digest.Pure.SHA (sha256, showDigest)
import System.Directory
( copyFile
, createDirectoryIfMissing
, doesDirectoryExist
, doesFileExist
, getTemporaryDirectory
, listDirectory
, removeDirectoryRecursive
, removeFile
)
import System.Environment (lookupEnv)
import System.Exit (ExitCode (..))
import System.FilePath (dropExtension, takeFileName, (</>))
import System.Info (arch, os)
import System.Process (proc, readCreateProcessWithExitCode)
import Keel.Dyn.Locate (keelNativeDir)
-- | Why an installation could not happen.
data SetupError
= UnsupportedPlatform String String
-- ^ (capability, guidance) — no pinned artifact for this OS\/arch;
-- the guidance says what to do instead.
| DownloadFailed String String
-- ^ (url, tool output).
| ChecksumMismatch String String String
-- ^ (url, expected, got) — the archive is deleted before this is
-- thrown.
| ExtractFailed String String
-- ^ (archive, tool output).
deriving (Eq, Show)
instance Exception SetupError
data Artifact = Artifact
{ artCapability :: String
-- ^ keel data-dir name ('keelNativeDir' argument).
, artUrl :: String
, artSha256 :: String
-- ^ Lowercase hex, pinned.
, artLibSubdir :: FilePath
-- ^ Directory inside the archive whose files become the payload.
, artAttribution :: String
-- ^ Upstream project + license, recorded next to the payload.
}
-- | Install OpenBLAS (LP64) for keel-linalg. Pinned artifact exists for
-- Windows x86_64 (upstream publishes binaries only there); on other
-- platforms the package manager is the right tool and 'setupBlas'
-- returns the exact command as 'UnsupportedPlatform' guidance.
setupBlas :: IO (Either SetupError FilePath)
setupBlas = case (os, arch) of
("mingw32", "x86_64") ->
install
Artifact
{ artCapability = "openblas"
, artUrl = "https://github.com/OpenMathLib/OpenBLAS/releases/download/v0.3.30/OpenBLAS-0.3.30-x64.zip"
, artSha256 = "8b04387766efc05c627e26d24797ec0d4ed4c105ec14fa7400aa84a02db22b66"
, artLibSubdir = "bin"
, artAttribution =
"libopenblas.dll from OpenBLAS 0.3.30 (BSD-3-Clause), official release archive:\n\
\https://github.com/OpenMathLib/OpenBLAS/releases/tag/v0.3.30\n"
}
("linux", _) ->
pure (Left (UnsupportedPlatform "openblas"
"upstream publishes no Linux binaries; run: sudo apt-get install libopenblas0 (or your distro's equivalent)"))
("darwin", _) ->
pure (Left (UnsupportedPlatform "openblas"
"upstream publishes no macOS binaries; run: brew install openblas, then set KEEL_OPENBLAS=$(brew --prefix openblas)/lib/libopenblas.dylib"))
_ ->
pure (Left (UnsupportedPlatform "openblas" (os <> "/" <> arch <> " has no pinned artifact")))
-- | Install ONNX Runtime for keel-onnx from the official (MIT) release
-- archives: win-x64, linux-x64 and osx-arm64 are pinned.
setupOnnx :: IO (Either SetupError FilePath)
setupOnnx = case (os, arch) of
("mingw32", "x86_64") ->
install (onnxArtifact "onnxruntime-win-x64-1.24.4.zip"
"d2319fddfb6ea4db99ccc4b60c85c517bcd855721f5daa6a06d40d7cb2ee2357")
("linux", "x86_64") ->
install (onnxArtifact "onnxruntime-linux-x64-1.24.4.tgz"
"3a211fbea252c1e66290658f1b735b772056149f28321e71c308942cdb54b747")
("darwin", "aarch64") ->
install (onnxArtifact "onnxruntime-osx-arm64-1.24.4.tgz"
"93787795f47e1eee369182e43ed51b9e5da0878ab0346aecf4258979b8bba989")
_ ->
pure (Left (UnsupportedPlatform "onnxruntime"
(os <> "/" <> arch <> " has no pinned artifact; official archives cover win-x64, linux-x64, osx-arm64")))
where
onnxArtifact file sha =
Artifact
{ artCapability = "onnxruntime"
, artUrl = "https://github.com/microsoft/onnxruntime/releases/download/v1.24.4/" <> file
, artSha256 = sha
, -- archives unpack as <basename minus .zip/.tgz>/lib/...
artLibSubdir = dropExtension file </> "lib"
, artAttribution =
"ONNX Runtime 1.24.4 (MIT), official release archive:\n\
\https://github.com/microsoft/onnxruntime/releases/tag/v1.24.4\n"
}
-- ---------------------------------------------------------------------
install :: Artifact -> IO (Either SetupError FilePath)
install art = do
destDir <- keelNativeDir (artCapability art)
createDirectoryIfMissing True destDir
tmp <- getTemporaryDirectory
let archPath = tmp </> takeFileName (artUrl art)
exDir = tmp </> (artCapability art <> "-keel-extract")
(dlCode, _, dlErr) <-
readCreateProcessWithExitCode (proc "curl" ["-fsSL", "-o", archPath, artUrl art]) ""
case dlCode of
ExitFailure _ -> pure (Left (DownloadFailed (artUrl art) dlErr))
ExitSuccess -> do
got <- showDigest . sha256 <$> BL.readFile archPath
if got /= artSha256 art
then do
removeFile archPath
pure (Left (ChecksumMismatch (artUrl art) (artSha256 art) got))
else do
exExists <- doesDirectoryExist exDir
when exExists (removeDirectoryRecursive exDir)
createDirectoryIfMissing True exDir
t <- tarExe
(exCode, _, exErr) <-
readCreateProcessWithExitCode (proc t ["-xf", archPath, "-C", exDir]) ""
case exCode of
ExitFailure _ -> do
removeFile archPath
pure (Left (ExtractFailed archPath exErr))
ExitSuccess -> do
let srcLib = exDir </> artLibSubdir art
entries <- listDirectory srcLib
files <- filterM (doesFileExist . (srcLib </>)) entries
if null files
then do
removeFile archPath
removeDirectoryRecursive exDir
pure (Left (ExtractFailed archPath ("no payload files under " <> artLibSubdir art)))
else do
forM_ files $ \f -> copyFile (srcLib </> f) (destDir </> f)
writeFile (destDir </> "ATTRIBUTION.txt") (artAttribution art)
removeFile archPath
removeDirectoryRecursive exDir
pure (Right destDir)
-- On Windows, PATH often puts GNU tar (MSYS/Git) first, which cannot
-- unpack zip; the System32 bsdtar can, so use it by absolute path.
tarExe :: IO FilePath
tarExe = case os of
"mingw32" -> do
root <- lookupEnv "SystemRoot"
pure (maybe "tar" (\r -> r </> "System32" </> "tar.exe") root)
_ -> pure "tar"