Skip to content

Commit

Permalink
New IO interface to scan for Gentoo eclass vars
Browse files Browse the repository at this point in the history
Uses the `portageq` command to scan for repositories, which in turn are
scanned for eclasses, which are then scanned for eclass variables.

The variables are scanned using a heuristic which looks for

    "# @ECLASS_VARIABLE: "

at the start of each line, which means only properly documented
variables will be found.

Signed-off-by: hololeap <hololeap@users.noreply.github.com>
  • Loading branch information
hololeap committed Aug 4, 2023
1 parent e3d8483 commit 08ae7ef
Show file tree
Hide file tree
Showing 2 changed files with 159 additions and 0 deletions.
1 change: 1 addition & 0 deletions ShellCheck.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ library
ShellCheck.Regex
other-modules:
Paths_ShellCheck
ShellCheck.PortageVariables
ShellCheck.PortageAutoInternalVariables
default-language: Haskell98

Expand Down
158 changes: 158 additions & 0 deletions src/ShellCheck/PortageVariables.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@

module ShellCheck.PortageVariables
( RepoName
, RepoPath
, EclassVar
, Repository(..)
, Eclass(..)
, portageVariables
, scanRepos
) where

import Control.Applicative
import Control.Monad
import Control.Monad.Trans.Class
import Control.Monad.Trans.Maybe
import Data.Map (Map)
import qualified Data.Map as M
import System.Directory (listDirectory)
import System.Exit (ExitCode(..))
import System.FilePath
import System.Process -- (readProcessWithExitCode)
import Text.Parsec hiding ((<|>))
import Text.Parsec.String

type RepoName = String
type RepoPath = FilePath
type EclassVar = String

data Repository = Repository
{ repositoryName :: RepoName
, repositoryLocation :: RepoPath
, repositoryEclasses :: [Eclass]
} deriving (Show, Eq, Ord)

data Eclass = Eclass
{ eclassName :: String
, eclassVars :: [EclassVar]
} deriving (Show, Eq, Ord)

portageVariables :: [Repository] -> Map String [EclassVar]
portageVariables = foldMap $ foldMap go . repositoryEclasses
where
go e = M.singleton (eclassName e) (eclassVars e)

-- | Run @portageq@ to gather a list of repo names and paths, then scan each
-- one for eclasses and ultimately eclass metadata.
scanRepos :: IO [Repository]
scanRepos = do
let cmd = "/usr/bin/portageq"
let args = ["repos_config", "/"]
out <- runOrDie cmd args
case parse reposParser "scanRepos" out of
Left pe -> fail $ show pe
Right nps -> do
forM nps $ \(n,p) -> Repository n p <$> getEclasses p

-- | Get the name of the repo and its path from blocks outputted by
-- @portageq@. If the path doesn't exist, this will return @Nothing@.
reposParser :: Parser [(RepoName, RepoPath)]
reposParser =
choice
[ [] <$ eof
, repoName >>= repoBlock
]
where
-- Get the name of the repo at the top of the block
repoName :: Parser RepoName
repoName
= char '['
*> manyTill anyChar (try (char ']'))
<* endOfLine

-- Parse the block for location field
repoBlock :: RepoName -> Parser [(RepoName, RepoPath)]
repoBlock n = choice
[ try $ do
l <- string "location = " *> takeLine
-- Found the location, skip the rest of the block
skipMany miscLine *> endOfBlock
insert (n,l)
-- Did not find the location, keep trying
, try $ miscLine *> repoBlock n
-- Reached the end of the block, no location field
, endOfBlock *> ignore
]

miscLine :: Parser ()
miscLine = skipNonEmptyLine

-- A block ends with an eol or eof
endOfBlock :: Parser ()
endOfBlock = void endOfLine <|> eof

insert :: (RepoName, RepoPath) -> Parser [(RepoName, RepoPath)]
insert r = (r:) <$> reposParser

ignore :: Parser [(RepoName, RepoPath)]
ignore = reposParser

-- | Scan the repo path for @*.eclass@ files in @eclass/@, then run
-- 'eclassParser' on each of them to produce @[Eclass]@.
--
-- If the @eclass/@ directory doesn't exist, the scan is skipped for that
-- repo.
getEclasses :: RepoPath -> IO [Eclass]
getEclasses repoLoc = fmap (maybe [] id) $ runMaybeT $ do
let eclassDir = repoLoc </> "eclass"

-- Silently fail if the repo doesn't have an eclass dir
fs <- MaybeT $ Just <$> listDirectory eclassDir <|> pure Nothing
let fs' = filter (\(_,e) -> e == ".eclass") $ map splitExtensions fs

forM fs' $ \(n,e) -> do
evs <- lift $ parseFromFile eclassParser (eclassDir </> n <.> e)
case evs of
Left pe -> lift $ fail $ show pe
Right vs -> pure $ Eclass n vs

eclassParser :: Parser [EclassVar]
eclassParser = choice
[ -- cons the EclassVar to the list and continue
try $ liftA2 (:) eclassVar eclassParser
-- or skip the line and continue
, skipLine *> eclassParser
-- or end the list on eof
, [] <$ eof
]
where
-- Scans for @ECLASS_VARIABLE comments rather than parsing the raw bash
eclassVar :: Parser EclassVar
eclassVar = string "# @ECLASS_VARIABLE: " *> takeLine

takeLine :: Parser String
takeLine = manyTill anyChar (try endOfLine)

-- | Fails if next char is 'endOfLine'
skipNonEmptyLine :: Parser ()
skipNonEmptyLine = notFollowedBy endOfLine *> skipLine

skipLine :: Parser ()
skipLine = void takeLine

-- | Run the command and return the full stdout string (stdin is ignored).
--
-- If the command exits with a non-zero exit code, this will throw an
-- error including the captured contents of stdout and stderr.
runOrDie :: FilePath -> [String] -> IO String
runOrDie cmd args = do
(ec, o, e) <- readProcessWithExitCode cmd args ""
case ec of
ExitSuccess -> pure o
ExitFailure i -> fail $ unlines $ map unwords
$ [ [ show cmd ]
++ map show args
++ [ "failed with exit code", show i]
, [ "stdout:" ], [ o ]
, [ "stderr:" ], [ e ]
]

0 comments on commit 08ae7ef

Please sign in to comment.