ligo/nix/opam-parser.hs

202 lines
7.6 KiB
Haskell
Raw Normal View History

2020-03-25 22:14:18 +04:00
{-# LANGUAGE FlexibleContexts, LambdaCase, RecordWildCards #-}
import Text.Parsec
import Data.Functor.Identity (Identity ())
import System.Environment (getEnv)
import Data.Maybe (fromJust, isNothing, maybeToList)
import Control.Monad (void)
import Data.List (intersperse, nub, isPrefixOf)
import qualified Control.Applicative as A (optional)
data OPAM
= OPAM
{ name :: Maybe String
, version :: Maybe String
2020-03-26 03:48:32 +04:00
, nativeBuildInputs :: Maybe [String]
, buildInputs :: Maybe [String]
, buildPhase :: Maybe [[String]]
, checkInputs :: Maybe [String]
, checkPhase :: Maybe [[String]]
, source :: Maybe String
2020-03-25 22:14:18 +04:00
} deriving Show
2020-03-26 14:38:56 +04:00
-- Turn a description into a nix file
2020-03-25 22:14:18 +04:00
opam2nix :: OPAM -> String
opam2nix OPAM {..} =
2020-03-26 03:48:32 +04:00
let
normalize = nub . map (\case 'b':'a':'s':'e':'-':_ -> "base"; s -> s)
buildInputs' = [ "findlib" ] ++ mconcat (maybeToList buildInputs);
checkInputs' = mconcat $ maybeToList checkInputs
nativeBuildInputs' = [ "dune", "opaline", "ocaml", "findlib" ]
++ (if any (isPrefixOf "conf-")
(buildInputs' ++ checkInputs' ++ mconcat (maybeToList nativeBuildInputs))
then ["conf-pkg-config"]
else [])
++ mconcat (maybeToList nativeBuildInputs)
inputs = buildInputs' ++ checkInputs' ++ nativeBuildInputs'
deps = mconcat $ intersperse ", " $ normalize $ inputs
sepspace = mconcat . intersperse " " . normalize
preparephase = mconcat . intersperse " " . mconcat . intersperse ["\n"]
2020-03-25 22:14:18 +04:00
in
2020-03-26 21:24:04 +04:00
"{ stdenv, fetchzip, " <>deps<> ", extraArgs ? { } }:\n"
2020-03-26 21:27:16 +04:00
<>"\n" -- Awful hack to allow this to evaluate even if some of the variables are undefined
<>"stdenv.mkDerivation (let self = with self; with extraArgs; {\n"
2020-03-26 03:48:32 +04:00
<>foldMap (\name' -> " pname = \""<>name'<>"\";\n") name
<>foldMap (\version' -> " version = \""<>version'<>"\";\n") version
2020-03-25 22:14:18 +04:00
<>foldMap (\url -> " src = builtins.fetchTarball { url = \""<>url<>"\"; };\n") source
2020-03-26 03:48:32 +04:00
<>" buildInputs = [ "<>sepspace buildInputs'<>" ];\n"
<>" checkInputs = [ "<>sepspace checkInputs'<>" ];\n"
<>" nativeBuildInputs = [ "<>sepspace nativeBuildInputs'<>" ];\n"
2020-03-25 22:14:18 +04:00
<>" propagatedBuildInputs = buildInputs;\n"
2020-03-26 03:48:32 +04:00
<>" propagatedNativeBuildInputs = nativeBuildInputs;\n"
<>foldMap (\buildPhase' ->
" buildPhase = ''runHook preBuild\n"
<> preparephase buildPhase'
<>"\nrunHook postBuild\n'';\n") buildPhase
<>foldMap (\checkPhase' ->
" checkPhase = ''runHook preCheck\n"
<>preparephase checkPhase'
<>"\nrunHook postCheck\n'';\n") checkPhase
<>" installPhase = ''\nrunHook preInstall\nopaline -prefix $out -libdir $OCAMLFIND_DESTDIR\nrunHook postInstall\n'';\n"
2020-03-26 21:27:16 +04:00
<>"}; in self // extraArgs)\n"
2020-03-25 22:14:18 +04:00
2020-03-26 03:48:32 +04:00
update :: Maybe a -> a -> Maybe a
update old new = if isNothing old then Just new else old
2020-03-25 22:14:18 +04:00
2020-03-26 14:38:56 +04:00
-- Evaluate a Field and update OPAM description accordingly
2020-03-25 22:14:18 +04:00
evaluateField :: OPAM -> Field -> OPAM
2020-03-26 03:48:32 +04:00
evaluateField o@OPAM {..} = \case
Name s -> o { name = update name s }
Version s -> o { version = update version s }
Depends s -> o {
buildInputs = update buildInputs $
fmap identifier $ filter (\(Package _ info) -> not $ ("with-test" `elem` info || "build" `elem` info)) s,
nativeBuildInputs = update nativeBuildInputs $
fmap identifier $ filter (\(Package _ info) -> "build" `elem` info) s,
checkInputs = update checkInputs $
fmap identifier $ filter (\(Package _ info) -> "with-test" `elem` info) s
}
Build e -> o {
buildPhase = update buildPhase
$ fmap ((fmap evaluateExp) . command) $ filter (\(Command _ info) -> not $ "with-test" `elem` info) e,
checkPhase = update checkPhase
$ fmap ((fmap evaluateExp) . command) $ filter (\(Command _ info) -> "with-test" `elem` info) e
}
URL url -> o { source = update source url}
2020-03-25 22:14:18 +04:00
Other _ -> o
evaluateFields :: OPAM -> [Field] -> OPAM
evaluateFields = foldl evaluateField
2020-03-26 14:38:56 +04:00
-- Descriptions for various Fields of an opam file
2020-03-26 03:48:32 +04:00
data Package
= Package
{ identifier :: String
, additionalPackageInfo :: [String]
} deriving Show
2020-03-26 14:38:56 +04:00
-- An expression as found in a Command
2020-03-26 03:48:32 +04:00
data Exp = Str String | Var String deriving Show
evaluateExp :: Exp -> String
evaluateExp =
let
repl ('%':'{':xs) = '$':'{':repl xs
2020-03-26 04:18:13 +04:00
repl ('}':'%':xs) = '}':repl xs
2020-03-26 03:48:32 +04:00
repl (':':_:_:_:'}':'%':xs) = '}':repl xs
repl (x:xs) = x:repl xs
repl "" = ""
in
\case
Str s -> repl s
Var "name" -> "${pname}"
Var "make" -> "make"
Var "prefix" -> "$out"
Var "jobs" -> "1"
Var s -> "${"<>s<>"}"
2020-03-25 22:14:18 +04:00
2020-03-26 14:38:56 +04:00
data Command
= Command
{ command :: [Exp]
, additionalCommandInfo :: [String]
} deriving Show
data Field
= Name String
| Version String
| Depends [Package]
| Build [Command]
| URL String
| Other String
deriving Show
-- An opam file is a collection of fields,
2020-03-25 22:14:18 +04:00
opamFile :: ParsecT String u Identity [Field]
opamFile = many field <* eof
2020-03-26 14:38:56 +04:00
-- Each has a name and a type;
2020-03-25 22:14:18 +04:00
field :: ParsecT String u Identity Field
field = Name <$> fieldParser "name" stringParser
<|> Version <$> fieldParser "version" stringParser
<|> Depends <$> fieldParser "depends" (listParser packageParser)
<|> Build <$> fieldParser "build" (pure <$> try commandParser <|> listParser commandParser)
<|> sectionParser "url" (URL <$> (fieldParser "src" stringParser <* many (noneOf "}")))
2020-03-26 03:48:32 +04:00
<|> Other <$> (many (noneOf "\n") <* char '\n')
2020-03-25 22:14:18 +04:00
2020-03-26 14:38:56 +04:00
-- Field's structure is "name: value"
2020-03-25 22:14:18 +04:00
fieldParser :: String -> ParsecT String u Identity t -> ParsecT String u Identity t
2020-03-26 03:48:32 +04:00
fieldParser name valueParser = try $ between (string (name<>":") >> many (oneOf " \n")) (many $ oneOf " \n") valueParser <* commentParser
2020-03-25 22:14:18 +04:00
2020-03-26 14:38:56 +04:00
-- Sections's structure is "name { fields }"
2020-03-25 22:14:18 +04:00
sectionParser :: String -> ParsecT String u Identity t -> ParsecT String u Identity t
sectionParser name valueParser = try $ between (string name >> many (oneOf " ") >> string "{" >> many (oneOf " \n")) (many (oneOf " \n") >> char '}' >> char '\n') valueParser
2020-03-26 14:38:56 +04:00
-- String is enclosed in quotes
2020-03-25 22:14:18 +04:00
stringParser :: ParsecT String u Identity String
stringParser = between (char '"') (char '"') (many $ noneOf "\"")
2020-03-26 14:38:56 +04:00
-- Expression is either a string or a variable
2020-03-25 22:14:18 +04:00
expParser :: ParsecT String u Identity Exp
2020-03-26 03:48:32 +04:00
expParser = try (Str <$> stringParser)
<|> Var <$> many1 (noneOf " \n\"{}[]")
2020-03-26 14:38:56 +04:00
-- "Additional Info" is additional information about a package or command, "{like-this}"
2020-03-26 03:48:32 +04:00
additionalInfoParser :: ParsecT String u Identity [String]
additionalInfoParser = option [] $ try
$ between (many (char ' ') >> char '{') (char '}')
((many $ noneOf " &}") `sepBy` (oneOf " &"))
2020-03-25 22:14:18 +04:00
2020-03-26 14:38:56 +04:00
-- Command is a [expressions] with additionional information
2020-03-26 03:48:32 +04:00
commandParser :: ParsecT String u Identity Command
2020-03-26 14:38:56 +04:00
commandParser = Command <$> (listParser $ try expParser) <*> additionalInfoParser
2020-03-25 22:14:18 +04:00
2020-03-26 14:38:56 +04:00
-- Comment starts with # and goes to the end of line
2020-03-25 22:14:18 +04:00
commentParser :: ParsecT String u Identity ()
commentParser = optional $ do
void $ string "#"
many $ noneOf "\n"
2020-03-26 14:38:56 +04:00
-- Package is a "string" with additional information
2020-03-25 22:14:18 +04:00
packageParser :: ParsecT String u Identity Package
2020-03-26 14:38:56 +04:00
packageParser = Package <$> stringParser <*> additionalInfoParser
2020-03-25 22:14:18 +04:00
listParser :: ParsecT String u Identity t -> ParsecT String u Identity [t]
listParser valueParser =
between (char '[') (char ']') $ between startPadding endPadding
valueParser `sepBy` sep
where
startPadding = sep
2020-03-26 03:48:32 +04:00
endPadding = whiteSpace
2020-03-25 22:14:18 +04:00
sep = (whiteSpace >> commentParser) <|> whiteSpace
2020-03-26 03:48:32 +04:00
whiteSpace = optional $ many $ oneOf " \n"
2020-03-25 22:14:18 +04:00
main :: IO ()
2020-03-26 21:24:04 +04:00
main = getContents >>= \s -> case parse opamFile "(unknown)" s of
2020-03-26 03:48:32 +04:00
Left e -> print e
2020-03-26 21:24:04 +04:00
Right fs -> putStrLn $ opam2nix $ evaluateFields (OPAM Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing) fs
2020-03-26 03:48:32 +04:00
-- Right fs -> mapM_ print fs