module Knives.Kernel
( knifeKernel
, KernelOptions(..)
) where
import CommandLine (KernelOptions(..))
import System.Process (readProcess)
import Control.Exception (try, IOException)
import Data.Char (isSpace)
import Data.List (dropWhileEnd, isInfixOf, isPrefixOf)
import System.Directory (doesFileExist)
import Data.Maybe (catMaybes)
-- | Trim leading and trailing whitespace/newlines
trim :: String -> String
trim = dropWhileEnd isSpace . dropWhile isSpace
-- | Safely run a command. Returns Nothing if it fails or command not found.
tryReadProcess :: String -> [String] -> IO (Maybe String)
tryReadProcess cmd args = do
result <- try @IOException (readProcess cmd args "")
pure $ either (const Nothing) (Just . trim) result
-- | Detect Debian-based systems (Debian, Ubuntu, Mint, Pop!_OS, Kali, etc.)
isDebianBased :: IO Bool
isDebianBased = do
hasDebian <- doesFileExist "/etc/debian_version"
if hasDebian
then pure True
else checkOsRelease
where
checkOsRelease = do
hasOs <- doesFileExist "/etc/os-release"
if not hasOs then pure False else do
content <- readFile "/etc/os-release"
let debianLike = ["debian", "ubuntu", "linuxmint", "pop", "kali", "elementary", "zorin"]
pure $ any (\idName ->
("ID=" ++ idName) `isInfixOf` content ||
("ID_LIKE=" ++ idName) `isInfixOf` content
) debianLike
-- | Detect Red Hat family (RHEL, CentOS, Fedora, Rocky, AlmaLinux, Oracle, etc.)
isRedHatBased :: IO Bool
isRedHatBased = do
hasRedhat <- doesFileExist "/etc/redhat-release"
if hasRedhat
then pure True
else checkOsRelease
where
checkOsRelease = do
hasOs <- doesFileExist "/etc/os-release"
if not hasOs then pure False else do
content <- readFile "/etc/os-release"
let redhatLike = ["rhel", "centos", "fedora", "rocky", "almalinux", "ol", "scientific"]
pure $ any (\idName ->
("ID=" ++ idName) `isInfixOf` content ||
("ID_LIKE=" ++ idName) `isInfixOf` content
) redhatLike
-- | Get the currently running kernel (works on all Linux distros)
getRunningKernel :: IO String
getRunningKernel = do
mVer <- tryReadProcess "uname" ["-r"]
pure $ maybe "unknown" id mVer
-- | Get installed kernel packages (distro-aware)
getInstalledKernels :: IO [String]
getInstalledKernels = do
debian <- isDebianBased
if debian
then getDebianKernels
else do
redhat <- isRedHatBased
if redhat
then getRedHatKernels
else getArchKernels
-- | Debian / Ubuntu / Mint style
getDebianKernels :: IO [String]
getDebianKernels = do
mOut <- tryReadProcess "dpkg-query"
["-W", "-f=${Package} ${Version}\n", "linux-image-*"]
case mOut of
Just out | not (null out) ->
pure $ filter (not . null) $ lines out
_ -> do
-- Fallback to dpkg -l
mOut2 <- tryReadProcess "dpkg" ["-l"]
case mOut2 of
Just out2 ->
pure $ take 8
$ map (unwords . take 4 . words)
$ filter ("linux-image" `isInfixOf`) (lines out2)
Nothing -> pure []
-- | Red Hat family (RHEL, CentOS, Fedora, Rocky, AlmaLinux, etc.)
getRedHatKernels :: IO [String]
getRedHatKernels = do
mOut <- tryReadProcess "rpm"
[ "-qa"
, "--qf"
, "%{NAME}-%{VERSION}-%{RELEASE}\n"
, "kernel*"
]
case mOut of
Just out | not (null (trim out)) ->
pure $ take 8
$ filter (\pkg ->
"kernel" `isPrefixOf` pkg &&
not ("headers" `isInfixOf` pkg) &&
not ("devel" `isInfixOf` pkg) &&
not ("doc" `isInfixOf` pkg) &&
not ("tools" `isInfixOf` pkg)
)
$ lines (trim out)
_ -> pure []
-- | Arch Linux style
getArchKernels :: IO [String]
getArchKernels = do
mLinux <- tryReadProcess "pacman" ["-Q", "linux"]
mLts <- tryReadProcess "pacman" ["-Q", "linux-lts"]
mZen <- tryReadProcess "pacman" ["-Q", "linux-zen"]
mHard <- tryReadProcess "pacman" ["-Q", "linux-hardened"]
pure $ catMaybes [mLinux, mLts, mZen, mHard]
-- | Main entry point for the `sak kernel` command
knifeKernel :: KernelOptions -> IO ()
knifeKernel optsK = do
running <- getRunningKernel
if justVersion optsK
then putStrLn running
else do
installed <- getInstalledKernels
putStrLn $ "Running kernel: " ++ running
if null installed
then putStrLn "Installed kernels: (could not determine on this distribution)"
else do
putStrLn "Installed kernels:"
mapM_ (putStrLn . (" " ++)) installed