{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ViewPatterns #-}
import Control.Monad.Extra (ifM)
import qualified Data.ByteString as BS
import Data.List (sort)
import Data.List.Extra (nubOn)
import Data.List.NonEmpty (NonEmpty, nonEmpty)
import qualified Data.List.NonEmpty as NEL
import qualified Data.Map.Strict as M
import Data.Maybe (fromMaybe, mapMaybe)
import Data.Semigroup.Foldable (fold1)
import Data.Text (Text)
import qualified Data.Text as T
import Data.Text.Encoding (encodeUtf8)
import Distribution.Pretty (prettyShow)
import Distribution.Types.PackageId (PackageIdentifier (..))
import Distribution.Types.PackageName (PackageName, mkPackageName,
unPackageName)
import OpenSSL (withOpenSSL)
import qualified Options.Applicative as Opts
import Stackage
import System.Directory (doesDirectoryExist,
listDirectory)
import System.FilePath (addTrailingPathSeparator,
takeBaseName, takeDirectory,
takeExtension, (</>))
main :: IO ()
main = withOpenSSL $ do
Options{input} <- Opts.execParser $
Opts.info (Opts.helper <*> optionsParser) Opts.fullDesc
text <- BS.readFile input
stack <- readStack text
let
dir = (takeDirectory input)
resolvers <- unroll dir stack
let
resolver = fold1 resolvers
project = genProject stack resolver
dirs = NEL.toList $ (dir </>) <$> localDirs project
cabals <- concat <$> traverse (globExt ".cabal") dirs
let
-- assumes that .cabal files are named correctly, otherwise we need
-- PackageDescription-Parsec.html#v:readGenericPackageDescription
ignore = (mkPackageName . takeBaseName) <$> cabals
freeze = genFreeze resolver ignore
BS.writeFile (dir </> "cabal.project") (encodeUtf8 $ printProject project)
BS.writeFile (dir </> "cabal.project.freeze") (encodeUtf8 $ printFreeze freeze)
globExt :: String -> FilePath -> IO [FilePath]
globExt ext path = do
files <- ifM (doesDirectoryExist path) (listDirectory path) (pure [])
pure $ filter ((ext ==) . takeExtension) files
printProject :: Project -> Text
printProject (Project (Ghc ghc) pkgs srcs) =
T.concat [ "-- Generated by stackage-to-hackage from stack.yaml\n\n"
, "with-compiler: ", ghc, "\n\n"
, "packages:\n ", packages, "\n\n"
, sources, "\n"
, "allow-older: *\n"
, "allow-newer: *\n"
]
where
packages = T.intercalate "\n , " (T.pack . addTrailingPathSeparator <$>
NEL.toList pkgs)
sources = T.intercalate "\n" (source =<< srcs)
source Git{repo, commit, subdirs} =
let base = T.concat [ "source-repository-package\n "
, "type: git\n "
, "location: ", repo, "\n "
, "tag: ", commit, "\n"]
in if null subdirs
then [base]
else (\d -> T.concat [base, " subdir: ", d, "\n"]) <$> subdirs
data Project = Project Ghc (NonEmpty FilePath) [Git] deriving (Show)
localDirs :: Project -> NonEmpty FilePath
localDirs (Project _ nefp _ ) = nefp
genProject :: Stack -> Resolver -> Project
genProject Stack{packages} Resolver{compiler, deps} = Project
(fromMaybe (Ghc "ghc") compiler)
(fromMaybe (pure ".") (nonEmpty $ mapMaybe pickLocal packages))
(nubOn repo $ mapMaybe pickGit deps)
where
pickLocal (Local p) = Just p
pickLocal (Location _) = Nothing
pickGit (Hackage _ ) = Nothing
pickGit (SourceDep g) = Just g
printFreeze :: Freeze -> Text
printFreeze (Freeze deps (Flags flags)) =
T.concat [ "constraints:\n ", constraints, "\n"]
where
constraints = T.intercalate "\n , " (constrait <$> sort deps)
constrait pkg =
let name = (T.pack . unPackageName . pkgName $ pkg)
ver = (T.pack . prettyShow . pkgVersion $ pkg)
base = T.concat [name, " ==", ver]
in case M.lookup name flags of
Nothing -> base
Just entries -> T.concat [ name, " ", (custom entries)
, "\n , ", base]
custom (M.toList -> lst) = T.intercalate " " $ (renderFlag <$> lst)
renderFlag (name, True) = "+" <> name
renderFlag (name, False) = "-" <> name
data Freeze = Freeze [PackageIdentifier] Flags deriving (Show)
genFreeze :: Resolver -> [PackageName] -> Freeze
genFreeze Resolver{deps, flags} ignore =
let pkgs = filter noSelfs $ unPkgId <$> mapMaybe pick deps
uniqpkgs = nubOn pkgName pkgs
in Freeze uniqpkgs flags
where pick (Hackage p) = Just p
pick (SourceDep _) = Nothing
noSelfs (pkgName -> n) = notElem n ignore
data Options = Options
{ input :: FilePath
}
optionsParser :: Opts.Parser Options
optionsParser = Options
<$> file
where
file = Opts.strArgument
( Opts.metavar "FILENAME"
<> Opts.help "Input stack.yaml")