{-# LANGUAGE OverloadedStrings, CPP #-} import Control.Monad.Extra import Data.Either import Data.Ini.Config import Data.List.Extra import Data.Maybe import Data.Tuple (swap) import SimpleCmd import SimpleCmdArgs import System.Directory import System.Exit import System.FilePath import System.IO import System.Process import qualified Data.Text.IO as T import MajorVer import Paths_stack_all (version) import Snapshots defaultOldestLTS :: MajorVer defaultOldestLTS = LTS 11 data VersionLimit = DefaultLimit | Oldest MajorVer | AllVersions data Command = CreateConfig | MakeStackLTS | DefaultRun main :: IO () main = do hSetBuffering stdout NoBuffering simpleCmdArgs' (Just version) "Build over Stackage versions" "stack-all builds projects easily across different Stackage versions" $ run <$> (flagWith' CreateConfig 'c' "create-config" "Create a project .stack-all file" <|> flagWith DefaultRun MakeStackLTS 's' "make-lts" "Create a stack-ltsXX.yaml file") <*> switchWith 'k' "keep-going" "Keep going even if an LTS fails" <*> switchWith 'd' "debug" "Verbose stack build output on error" <*> optional (readMajor <$> strOptionWith 'n' "newest" "MAJOR" "Newest LTS release to build from") <*> (Oldest . readMajor <$> strOptionWith 'o' "oldest" "MAJOR" "Oldest compatible LTS release" <|> flagWith DefaultLimit AllVersions 'a' "all-lts" "Try to build back to LTS 1 even") <*> many (strArg "MAJORVER... [COMMAND...]") run :: Command -> Bool ->Bool -> Maybe MajorVer -> VersionLimit -> [String] -> IO () run command keepgoing debug mnewest verlimit verscmd = do findStackProjectDir Nothing case command of CreateConfig -> case verlimit of Oldest oldest -> createStackAll (Just oldest) mnewest _ -> createStackAll Nothing mnewest MakeStackLTS -> do (versions, _) <- getVersionsCmd if null versions then error' "--make-lts needs an LTS major version" else makeStackLTS versions DefaultRun -> do (versions, cargs) <- getVersionsCmd configs <- mapMaybe readStackConf <$> listDirectory "." let newestFilter = maybe id (filter . (>=)) mnewest mapM_ (stackBuild configs keepgoing debug cargs) (newestFilter versions) where findStackProjectDir :: Maybe FilePath -> IO () findStackProjectDir mcwd = do haveStackYaml <- doesFileExist "stack.yaml" if haveStackYaml then return () else do cwdir <- getCurrentDirectory if cwdir /= "/" then setCurrentDirectory ".." >> findStackProjectDir (if isJust mcwd then mcwd else Just cwdir) else do putStrLn "stack.yaml not found" whenJust mcwd setCurrentDirectory haveCabalFile <- doesFileExistWithExtension "." ".cabal" if haveCabalFile then do -- FIXME take suggested extra-deps into stack.yaml -- FIXME stack init content too verbose unlessM (cmdBool "stack" ["init"]) $ -- FIXME determine latest stable snapshot automatically writeFile "stack.yaml" "resolver: lts-18.8\n" else error' "no package/project found" getVersionsCmd :: IO ([MajorVer],[String]) getVersionsCmd = do let partitionMajors = swap . partitionEithers . map eitherReadMajor (verlist,cmds) = partitionMajors verscmd allMajors <- getMajorVers versions <- if null verlist then case verlimit of DefaultLimit -> do (newestLTS, oldestLTS) <- readNewestOldestLTS return $ case mnewest of Just newest -> if newest < oldestLTS then filter (inRange newest (LTS 1)) allMajors else filter (inRange newest oldestLTS) allMajors Nothing -> filter (inRange newestLTS oldestLTS) allMajors AllVersions -> return allMajors Oldest ver -> return $ filter (inRange Nightly ver) allMajors else return verlist return (versions,if null cmds then ["build"] else cmds) where inRange :: MajorVer -> MajorVer -> MajorVer -> Bool inRange newest oldest v = v >= oldest && v <= newest readStackConf :: FilePath -> Maybe MajorVer readStackConf "stack-lts.yaml" = error' "unversioned stack-lts.yaml is unsupported" readStackConf f = stripPrefix "stack-" f >>= stripSuffix ".yaml" >>= readCompactMajor stackAllFile :: FilePath stackAllFile = ".stack-all" createStackAll :: Maybe MajorVer -> Maybe MajorVer -> IO () createStackAll Nothing Nothing = error' "creating .stack-all requires --oldest LTS and/or --newest LTS" createStackAll moldest mnewest = do exists <- doesFileExist stackAllFile if exists then error' $ stackAllFile ++ " already exists" else do allMajors <- getMajorVers writeFile stackAllFile $ "[versions]\n" ++ case mnewest of Nothing -> "" Just newest -> "newest = " ++ showMajor newest ++ "\n" ++ case moldest of Nothing -> "" Just oldest -> let older = let molder = listToMaybe $ dropWhile (>= oldest) allMajors in maybe "" (\s -> showMajor s ++ " too old") molder in "# " ++ older ++ "\noldest = " ++ showMajor oldest ++ "\n" readNewestOldestLTS :: IO (MajorVer,MajorVer) readNewestOldestLTS = do haveConfig <- doesFileExist stackAllFile if haveConfig then readIniConfig stackAllFile $ section "versions" $ do mnewest <- fmap readMajor <$> fieldMbOf "newest" string moldest <- fmap readMajor <$> fieldMbOf "oldest" string return (fromMaybe Nightly mnewest, fromMaybe defaultOldestLTS moldest) else return (Nightly, defaultOldestLTS) where readIniConfig :: FilePath -> IniParser a -> IO a readIniConfig inifile iniparser = do ini <- T.readFile inifile return $ either error id $ parseIniFile ini iniparser makeStackLTS :: [MajorVer] -> IO () makeStackLTS vers = do configs <- mapMaybe readStackConf <$> listDirectory "." forM_ vers $ \ ver -> do if ver `elem` configs then error' $ showConfig ver ++ " already exists!" else do let mcurrentconfig = listToMaybe $ sort (filter (ver <=) configs) case mcurrentconfig of Nothing -> copyFile "stack.yaml" (showConfig ver) Just conf -> copyFile (showConfig conf) (showConfig ver) whenJustM (latestSnapshot ver) $ \latest -> cmd_ "sed" ["-i", "-e", "s/\\(resolver:\\) .*/\\1 " ++ latest ++ "/", showConfig ver] showConfig :: MajorVer -> FilePath showConfig sn = "stack-" ++ compactMajor sn <.> "yaml" where compactMajor :: MajorVer -> String compactMajor Nightly = "nightly" compactMajor (LTS ver) = "lts" ++ show ver stackBuild :: [MajorVer] -> Bool -> Bool -> [String] -> MajorVer -> IO () stackBuild configs keepgoing debug command ver = do let config = case sort (filter (ver <=) configs) of [] -> [] (cfg:_) -> ["--stack-yaml", showConfig cfg] latest <- latestSnapshot ver case latest of Nothing -> error' $ "no snapshot not found for " ++ showMajor ver Just minor -> do let opts = ["-v" | debug] ++ ["--resolver", minor] ++ config putStrLn $ "# " ++ minor if debug then debugBuild $ opts ++ command else do ok <- cmdBool "stack" $ opts ++ command unless (ok || keepgoing) $ do putStr "\nsnapshot-pkg-db: " cmd_ "stack" $ "--silent" : opts ++ ["path", "--snapshot-pkg-db"] error' $ "failed for " ++ showMajor ver putStrLn "" where debugBuild :: [String] -> IO () debugBuild args = do putStr $ "stack " ++ unwords args (ret,out,err) <- readProcessWithExitCode "stack" args "" putStrLn "\n" unless (null out) $ putStrLn out unless (ret == ExitSuccess) $ do -- stack verbose includes info line with all stackages (> 500kbytes) mapM_ putStrLn $ filter ((<10000) . length) . lines $ err error' $ showMajor ver ++ " build failed" -- taken from cabal-rpm FileUtils: filesWithExtension :: FilePath -- directory -> String -- file extension -> IO [FilePath] filesWithExtension dir ext = filter (ext `isExtensionOf`) <$> listDirectory dir -- looks in dir for a unique file with given extension fileWithExtension :: FilePath -- directory -> String -- file extension -> IO (Maybe FilePath) fileWithExtension dir ext = do files <- filesWithExtension dir ext case files of [file] -> return $ Just $ dir file [] -> return Nothing _ -> putStrLn ("More than one " ++ ext ++ " file found!") >> return Nothing -- looks in current dir for a unique file with given extension doesFileExistWithExtension :: FilePath -> String -> IO Bool doesFileExistWithExtension dir ext = isJust <$> fileWithExtension dir ext #if !MIN_VERSION_filepath(1,4,2) isExtensionOf :: String -> FilePath -> Bool isExtensionOf ext@('.':_) = isSuffixOf ext . takeExtensions isExtensionOf ext = isSuffixOf ('.':ext) . takeExtensions #endif