diff options
| -rw-r--r-- | minici.cabal | 8 | ||||
| -rw-r--r-- | src/Config.hs | 94 | ||||
| -rw-r--r-- | src/Job.hs | 79 | ||||
| -rw-r--r-- | src/Main.hs | 115 | 
4 files changed, 210 insertions, 86 deletions
| diff --git a/minici.cabal b/minici.cabal index 8858889..692dafe 100644 --- a/minici.cabal +++ b/minici.cabal @@ -24,7 +24,8 @@ executable minici      ghc-options:      -Wall      -- Modules included in this executable, other than Main. -    -- other-modules: +    other-modules:      Config +                        Job      -- LANGUAGE extensions used by modules in this package.      default-extensions:  ExistentialQuantification @@ -35,15 +36,20 @@ executable minici                           ImportQualifiedPost                           LambdaCase                           MultiParamTypeClasses +                         MultiWayIf                           OverloadedStrings                           ScopedTypeVariables                           TupleSections                           TypeApplications      -- other-extensions:      build-depends:    base ^>=4.15.1.0 +                    , bytestring        >=0.10 && <0.11 +                    , containers        >=0.6 && <0.7                      , directory >=1.3 && <1.4                      , filepath >=1.4 && <1.5 +                    , HsYAML    >=0.2 && <0.3                      , mtl >=2.2 && <2.3 +                    , parser-combinators >=1.3 && <1.4                      , process >=1.6 && <1.7                      , text >=1.2 && <1.3      hs-source-dirs:   src diff --git a/src/Config.hs b/src/Config.hs new file mode 100644 index 0000000..7e9ad85 --- /dev/null +++ b/src/Config.hs @@ -0,0 +1,94 @@ +module Config ( +    Config(..), +    findConfig, +    parseConfig, +) where + +import Control.Monad +import Control.Monad.Combinators + +import Data.ByteString.Lazy qualified as BS +import Data.List +import Data.Map qualified as M +import Data.Maybe +import Data.Ord +import Data.Text (Text) +import Data.Text qualified as T +import Data.YAML + +import System.Directory +import System.Exit +import System.FilePath +import System.Process + +import Job + +data Config = Config +    { configJobs :: [Job] +    } + +instance Semigroup Config where +    a <> b = Config +        { configJobs = configJobs a ++ configJobs b +        } + +instance Monoid Config where +    mempty = Config +        { configJobs = [] +        } + +instance FromYAML Config where +    parseYAML = withMap "Config" $ \m -> do +        let getpos = \case (Scalar pos _, _) -> pos +                           (Mapping pos _ _, _) -> pos +                           (Sequence pos _ _, _) -> pos +                           (Anchor pos _ _, _) -> pos +        jobs <- fmap catMaybes $ forM (sortBy (comparing $ posLine . getpos) $ M.assocs m) $ \case +            (Scalar _ (SStr tag), node) | ["job", name] <- T.words tag -> do +                flip (withMap "Job") node $ \j -> Just <$> choice +                    [ cabalJob name =<< j .: "cabal" +                    , shellJob name =<< j .: "shell" +                    ] +            _ -> return Nothing +        return $ Config jobs + +cabalJob :: Text -> Node Pos -> Parser Job +cabalJob name = withMap "cabal job" $ \m -> do +    ghcOptions <- m .:? "ghc-options" >>= \case +        Nothing -> return [] +        Just s -> withSeq "GHC option list" (mapM (withStr "GHC option" return)) s + +    return Job +        { jobName = JobName name +        , jobRecipe = [ proc "cabal" $ concat [ ["build"], ("--ghc-option="++) . T.unpack <$> ghcOptions ] ] +        , jobArtifacts = map (\(ArtifactName aname) -> (ArtifactName "bin", proc "cabal" ["list-bin", T.unpack aname])) [] +        } + +shellJob :: Text -> Node Pos -> Parser Job +shellJob name = withSeq "shell commands" $ \xs -> do +    recipe <- forM xs $ withStr "shell command" $ return . T.unpack +    return Job +        { jobName = JobName name +        , jobRecipe = map shell recipe +        , jobArtifacts = [] +        } + +findConfig :: IO (Maybe FilePath) +findConfig = go "." +  where +    name = "minici.yaml" +    go path = do +        doesFileExist (path </> name) >>= \case +            True -> return $ Just $ path </> name +            False -> doesDirectoryExist (path </> "..") >>= \case +                True -> go (path </> "..") +                False -> return Nothing + +parseConfig :: FilePath -> IO Config +parseConfig path = do +    contents <- BS.readFile path +    case decode1 contents of +        Left (pos, err) -> do +            putStr $ prettyPosWithSource pos contents err +            exitFailure +        Right conf -> return conf diff --git a/src/Job.hs b/src/Job.hs new file mode 100644 index 0000000..db3f4d0 --- /dev/null +++ b/src/Job.hs @@ -0,0 +1,79 @@ +module Job ( +    Job(..), +    JobOutput(..), +    JobName(..), stringJobName, +    ArtifactName(..), +    runJob, +) where + +import Control.Monad +import Control.Monad.Except + +import Data.Text (Text) +import Data.Text qualified as T + +import System.Directory +import System.Exit +import System.FilePath +import System.IO +import System.Process + +data Job = Job +    { jobName :: JobName +    , jobRecipe :: [CreateProcess] +    , jobArtifacts :: [(ArtifactName, CreateProcess)] +    } + +data JobOutput = JobOutput +    { outName :: JobName +    , outStatus :: Bool +    , outArtifacts :: [(ArtifactName, FilePath)] +    } +    deriving (Show) + +data JobName = JobName Text +    deriving (Eq, Ord, Show) + +stringJobName :: JobName -> String +stringJobName (JobName name) = T.unpack name + +data ArtifactName = ArtifactName Text +    deriving (Eq, Ord, Show) + + +runJob :: FilePath -> String -> Job -> IO JobOutput +runJob dir cid job = do +    [path] <- lines <$> readProcess "mktemp" ["-d", "-t", "minici.XXXXXXXXXX"] "" + +    "" <- readProcess "git" ["--work-tree=" <> path, "restore", "--source=" <> cid, "--", "."] "" +    ["tree", tid]:_  <- map words . lines <$> readProcess "git" ["cat-file", "-p", cid] "" + +    let jdir = dir </> "jobs" </> tid </> stringJobName (jobName job) +    createDirectoryIfMissing True jdir +    logs <- openFile (jdir </> "log") WriteMode + +    res <- runExceptT $ do +        forM_ (jobRecipe job) $ \p -> do +            (Just hin, _, _, hp) <- liftIO $ createProcess_ "" p +                { cwd = Just path +                , std_in = CreatePipe +                , std_out = UseHandle logs +                , std_err = UseHandle logs +                } +            liftIO $ hClose hin +            exit <- liftIO $ waitForProcess hp + +            when (exit /= ExitSuccess) $ +                throwError () + +    hClose logs +    removeDirectoryRecursive $ path + +    writeFile (jdir </> "status") $ +        if res == Right () then "success\n" else "failure\n" + +    return JobOutput +        { outName = jobName job +        , outStatus = res == Right () +        , outArtifacts = [] +        } diff --git a/src/Main.hs b/src/Main.hs index e2d458f..da3c0d8 100644 --- a/src/Main.hs +++ b/src/Main.hs @@ -1,101 +1,46 @@  module Main (main) where  import Control.Monad -import Control.Monad.Except -import Data.Text (Text) -import Data.Text qualified as T - -import System.Directory -import System.Exit -import System.FilePath  import System.IO  import System.Process -data Job = Job -    { jobName :: JobName -    , jobRecipe :: [CreateProcess] -    , jobArtifacts :: [(ArtifactName, CreateProcess)] -    } - -data JobOutput = JobOutput -    { outName :: JobName -    , outStatus :: Bool -    , outArtifacts :: [(ArtifactName, FilePath)] -    } -    deriving (Show) - -data JobName = JobName Text -    deriving (Eq, Ord, Show) - -data ArtifactName = ArtifactName Text -    deriving (Eq, Ord, Show) - -stringJobName :: JobName -> String -stringJobName (JobName name) = T.unpack name - - -runJob :: FilePath -> String -> Job -> IO JobOutput -runJob dir cid job = do -    [path] <- lines <$> readProcess "mktemp" ["-d", "-t", "minici.XXXXXXXXXX"] "" - -    "" <- readProcess "git" ["--work-tree=" <> path, "restore", "--source=" <> cid, "--", "."] "" -    ["tree", tid]:_  <- map words . lines <$> readProcess "git" ["cat-file", "-p", cid] "" - -    let jdir = dir </> "jobs" </> tid </> stringJobName (jobName job) -    createDirectoryIfMissing True jdir -    logs <- openFile (jdir </> "log") WriteMode - -    res <- runExceptT $ do -        forM_ (jobRecipe job) $ \p -> do -            (Just hin, _, _, hp) <- liftIO $ createProcess_ "" p -                { cwd = Just path -                , std_in = CreatePipe -                , std_out = UseHandle logs -                , std_err = UseHandle logs -                } -            liftIO $ hClose hin -            exit <- liftIO $ waitForProcess hp +import Config +import Job -            when (exit /= ExitSuccess) $ -                throwError () - -    hClose logs -    removeDirectoryRecursive $ path - -    writeFile (jdir </> "status") $ -        if res == Right () then "success\n" else "failure\n" - -    return JobOutput -        { outName = jobName job -        , outStatus = res == Right () -        , outArtifacts = [] -        } - - -cabalJob :: JobName -> [ArtifactName] -> Job -cabalJob name artifacts = Job -    { jobName = name -    , jobRecipe = [ proc "cabal" ["build", "--ghc-option=-Werror"] ] -    , jobArtifacts = map (\(ArtifactName aname) -> (ArtifactName "bin", proc "cabal" ["list-bin", T.unpack aname])) artifacts -    } +fitToLength :: Int -> String -> String +fitToLength maxlen str | len <= maxlen = str <> replicate (maxlen - len) ' ' +                       | otherwise     = take (maxlen - 1) str <> "…" +    where len = length str  main :: IO ()  main = do +    Just configPath <- findConfig +    config <- parseConfig configPath +      commits <- map (fmap (drop 1) . (span (/=' '))) . lines <$>          readProcess "git" ["log", "--pretty=oneline", "--first-parent", "--reverse", "origin/master..HEAD"] "" -    forM_ commits $ \(cid, desc) -> do -        let descLen = length desc -            desc' = if descLen <= 50 then desc <> replicate (50 - descLen) ' ' -                                     else take 49 desc <> "…" -            shortCid = take 7 cid -        putStr $ shortCid <> " " <> desc' <> " " +    putStr $ replicate (8 + 50) ' ' +    forM_ (configJobs config) $ \job -> do +        putStr $ (' ':) $ fitToLength 7 $ stringJobName $ jobName job +    putStrLn "" + +    forM_ commits $ \(cid, desc) -> do +        let shortCid = take 7 cid +        putStr $ shortCid <> " " <> fitToLength 50 desc          hFlush stdout -        runJob "./.minici" cid (cabalJob (JobName "build") $ map (ArtifactName . T.pack) []) >>= \case -            out | outStatus out -> do -                putStr "\ESC[92m✓\ESC[0m" -            _ -> do -                putStr "\ESC[91m✗\ESC[0m" -                putStr $ "\r\ESC[91m" <> shortCid <> "\ESC[0m" +        results <- forM (configJobs config) $ \job -> do +            putStr " " +            hFlush stdout +            out <- runJob "./.minici" cid job +            if | outStatus out -> do +                    putStr "\ESC[92m✓\ESC[0m      " +               | otherwise -> do +                    putStr "\ESC[91m✗\ESC[0m      " +            hFlush stdout +            return $ outStatus out + +        when (not $ and results) $ do +            putStr $ "\r\ESC[91m" <> shortCid <> "\ESC[0m"          putStrLn "" |