{-# LANGUAGE CPP #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ViewPatterns #-}
{-# LANGUAGE RecordWildCards #-}
module Main where
import StackageToHackage.Hackage (printFreeze, printProject, stackToCabal)
import StackageToHackage.Hpack (hpackInput, execHpack)
import StackageToHackage.Stackage (localDirs, readStack)
import Control.Exception (throwIO)
import Control.Monad (filterM, when)
import Data.Dates.Parsing (parseDateTime, defaultConfigIO)
import Data.Foldable (traverse_)
import Data.Hourglass (timeConvert, Elapsed)
import Data.Maybe (mapMaybe)
import Data.Text (Text)
import Data.Text.Encoding (decodeUtf8, encodeUtf8)
import Options.Applicative
import Prelude hiding (lines)
import System.Directory (doesFileExist, makeAbsolute)
import System.FilePath (takeDirectory, (</>))
import qualified Data.ByteString as BS
import qualified Data.List.NonEmpty as NEL
import qualified Data.Text as T
version :: String
#ifdef CURRENT_PACKAGE_VERSION
version = CURRENT_PACKAGE_VERSION
#else
version = "unknown"
#endif
data Opts = Opts
{ input :: FilePath
, output :: Maybe FilePath
, inspectRemotes :: Bool
, pinGHC :: Bool
, sortRepos :: Bool
, runHpack :: Bool
, hackageIndexDate :: Maybe String -- ^ fuzzy date string
}
optsP :: Parser Opts
optsP =
Opts
<$> strOption
(short 'f'
<> long "file"
<> metavar "STACK_YAML"
<> help "Path to stack.yaml file"
<> value "stack.yaml"
<> showDefaultWith show
)
<*> optional
(strOption
(short 'o'
<> long "output-file"
<> metavar "CABAL_PROJECT"
<> help
"Path to output file (default: <project-dir>/cabal.project)"
<> showDefaultWith show
)
)
<*> (not <$> switch
(long "no-inspect-remotes"
<> help
"Don't check package names from remote git sources (this is faster, but may leave incorrect versions in cabal.project.freeze if remote packages overwrite stack resolver versions)"
)
)
<*> (not <$> switch
(long "no-pin-ghc" <> help "Don't pin the GHC version")
)
<*> (not <$> switch
(long "no-sort-repos" <> help "Don't sort the source repositories")
)
<*> (not <$> switch (long "no-run-hpack" <> help "Don't run hpack"))
<*> optional
(strOption
(short 'p'
<> long "pin-hackage-index"
<> metavar "FUZZY_DATE"
<> help "Pin hackage index state (values like \"now\" and \"yesterday\" work)"
)
)
main :: IO ()
main = do
let versionHelp = infoOption version (long "version" <> help "Show version" <> hidden)
customExecParser (prefs showHelpOnError) (info (optsP <**> helper <**> versionHelp) idm) >>= \Opts {..} -> do
-- read stack file
inDir <- makeAbsolute (takeDirectory input)
stack <- readStack =<< BS.readFile input
let subs = NEL.toList $ (inDir </>) <$> localDirs stack
when runHpack $ do
hpacks <-
filterM (doesFileExist . hpackInput) subs
traverse_ execHpack hpacks
-- run conversion
config <- defaultConfigIO
let dt :: Maybe Elapsed
dt = hackageIndexDate >>= \d -> fmap timeConvert
$ either (const Nothing) Just
$ parseDateTime config d
case (hackageIndexDate, dt) of
(Just d, Nothing) ->
throwIO $ userError ("Warning: failed to convert hackage index state date \""
<> d <> "\"")
_ -> pure ()
(project, freeze) <- stackToCabal inspectRemotes runHpack inDir stack
hack <- extractHack . decodeUtf8 <$> BS.readFile
(inDir </> "stack.yaml")
printText <- printProject pinGHC sortRepos dt project hack
-- write files
outFile <- case output of
Just output' -> makeAbsolute output'
Nothing -> pure (inDir </> "cabal.project")
BS.writeFile outFile (encodeUtf8 printText)
BS.writeFile
(outFile <> ".freeze")
(encodeUtf8 $ printFreeze freeze)
-- Backdoor allowing the stack.yaml to contain arbitrary text that will be
-- included in the cabal.project
extractHack :: Text -> Maybe Text
extractHack (T.split ('\n' ==) -> lines) =
let (_, region) = break (T.isPrefixOf "#+BEGIN_STACK2CABAL") lines
(hack, _) = break (T.isPrefixOf "#+END_STACK2CABAL") region
verbatim = mapMaybe (T.stripPrefix "# ") hack
in if null verbatim then Nothing else Just $ T.intercalate "\n" verbatim