{-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ViewPatterns #-} -- | A simplistic model of cabal multi-package files and convertors from Stackage. module StackageToHackage.Hackage ( Freeze(..) , Project(..) , printFreeze , printProject , stackToCabal ) where import StackageToHackage.Stackage ( localDirs, unroll, mergeResolvers ) import StackageToHackage.Stackage.Types ( Resolver(Resolver, compiler, deps, flags) , PkgId(unPkgId) , GhcFlags , GhcOptions(GhcOptions) , PackageGhcOpts(PackageGhcOpts) , Flags(..) , Dep(..) , Git(..) , Ghc(Ghc) , Stack(ghcOptions) ) import StackageToHackage.Hpack (hpackInput, execHpack) import StackageToHackage.Hackage.Types ( Constraint(..), Freeze(..), Project(..) ) import Control.Exception (throwIO) import Control.Monad (forM, when, void) import Control.Monad.Catch (handleIOError) import Data.Hourglass (timePrint, ISO8601_DateAndTime(..), Elapsed) import Data.List (nub, sort, sortOn, isSuffixOf, isPrefixOf) import Data.List.Extra (nubOrd, nubOrdOn, lower, dropPrefix, dropSuffix) import Data.List.NonEmpty (NonEmpty((:|))) import Data.Maybe (fromMaybe, mapMaybe, catMaybes) import Data.Text (Text) import Distribution.PackageDescription.Parsec (readGenericPackageDescription) import Distribution.Pretty (prettyShow) import Distribution.Types.GenericPackageDescription (GenericPackageDescription(..)) import Distribution.Types.PackageDescription (PackageDescription(..)) import Distribution.Types.PackageId (PackageIdentifier(..)) import Distribution.Types.PackageName (PackageName, unPackageName) import Distribution.Verbosity (silent) import Safe (headMay) import System.Exit (ExitCode(..)) import System.FilePath (()) import System.FilePattern.Directory (getDirectoryFiles) import System.IO (hPutStrLn, stderr) import System.IO.Temp (withSystemTempDirectory) import System.Process (withCreateProcess, proc, waitForProcess, StdStream(..), CreateProcess(..)) import qualified Data.List.NonEmpty as NEL import qualified Data.Map.Strict as M import qualified Data.Text as T import System.Directory (doesFileExist) -- | Converts a stack.yaml (and list of local packages) to cabal.project and -- cabal.project.freeze. stackToCabal :: Bool -- ^ whether to inspect remotes -> Bool -- ^ whether to run hpack -> FilePath -> Stack -> IO (Project, Freeze) stackToCabal inspectRemotes runHpack dir stack = do resolvers <- unroll dir stack let resolver = foldr1 mergeResolvers resolvers project = genProject stack resolver localPkgs <- fmap catMaybes . traverse (\f -> getPackageIdent (dir f)) . NEL.toList . pkgs $ project remotePkgs <- if inspectRemotes then getRemotePkgs (srcs project) runHpack else pure [] let ignore = sort . nub . fmap pkgName $ (localPkgs ++ remotePkgs) let freeze = genFreeze resolver ignore pure (project, freeze) -- | Extracts username/repo from an all lowercase repository string. -- -- >>> trimUserRepo "git@github.com:hasufell/stack2cabal.git" -- "hasufell/stack2cabal" -- -- >>> trimUserRepo "https://github.com/hasufell/stack2cabal.git" -- "hasufell/stack2cabal" trimUserRepo :: String -> String trimUserRepo s | "git@" `isPrefixOf` s = trimUserRepo $ dropPrefix "git@" s | "https://" `isPrefixOf` s = trimUserRepo $ dropPrefix "https://" s | "github.com" `isPrefixOf` s = trimUserRepo $ dropPrefix "github.com" s | "gitlab.com" `isPrefixOf` s = trimUserRepo $ dropPrefix "gitlab.com" s | "/" `isPrefixOf` s = trimUserRepo $ dropPrefix "/" s | ":" `isPrefixOf` s = trimUserRepo $ dropPrefix ":" s | ".git" `isSuffixOf` s = trimUserRepo $ dropSuffix ".git" s | otherwise = s printProject :: Bool -- ^ whether to pin GHC -> Bool -- ^ whether to sort source-repository-package items -> Maybe Elapsed -- ^ hackage index date to pin -> Project -> Maybe Text -> IO Text printProject pinGHC sortRepos indexDate (Project (Ghc ghc) pkgs srcs ghcOpts) hack = do ghcOpts' <- printGhcOpts ghcOpts pure $ T.concat $ ["-- Generated by stack2cabal\n\n"] <> withHackageIndex <> withCompiler <> [ "packages:\n ", packages, "\n\n", sources , "\n", "allow-older: *\n", "allow-newer: *\n" ] <> ghcOpts' <> verbatim hack where userRepo :: Git -> String userRepo Git{repo} = trimUserRepo . lower $ T.unpack repo repos = if sortRepos then sortOn userRepo srcs else srcs withHackageIndex :: [Text] withHackageIndex | (Just utc) <- indexDate = ["index-state: ", printUTC utc, "\n\n"] | otherwise = [] where printUTC :: Elapsed -> Text printUTC = T.pack . timePrint ISO8601_DateAndTime withCompiler :: [Text] withCompiler | pinGHC = ["with-compiler: ", ghc, "\n\n"] | otherwise = [] verbatim :: Maybe Text -> [Text] verbatim Nothing = [] verbatim (Just txt) = ["\n-- Verbatim\n", txt, "\n"] packages :: Text packages = T.intercalate "\n , " (T.pack . addTrailingPathSeparator' <$> NEL.toList pkgs) where addTrailingPathSeparator' :: FilePath -> FilePath addTrailingPathSeparator' x = if hasTrailingPathSeparator' x then x else x ++ ['/'] hasTrailingPathSeparator' :: FilePath -> Bool hasTrailingPathSeparator' "" = False hasTrailingPathSeparator' x = last x == '/' sources :: Text sources = T.intercalate "\n" (source =<< repos) source :: Git -> [Text] source Git { repo, commit, subdirs } = let base = T.concat [ "source-repository-package\n ", "type: git\n ", "location: " , repo, "\n ", "tag: ", commit, "\n" ] in case sort subdirs of [] -> [base] (x:xs) -> [T.concat $ [base, " subdir: ", x] ++ (("\n " <>) <$> xs) ++ ["\n"]] -- Get the ghc options. This requires IO, because we have to figure out -- the local package names. printGhcOpts :: GhcOptions -> IO [GhcFlags] printGhcOpts (GhcOptions locals _ everything (PackageGhcOpts packagesGhcOpts)) = do -- locals are basically pkgs since cabal-install-3.4.0.0 localsPrint <- case locals of Just x -> fmap concat $ forM pkgs $ \pkg -> do name <- fmap (unPackageName . pkgName) <$> getPackageIdent pkg pure $ maybe [] (\n -> if M.member n $ M.mapKeys (unPackageName . pkgName . unPkgId) packagesGhcOpts then [] else [ "\npackage ", T.pack n, "\n ", "ghc-options: ", x, "\n" ] ) name Nothing -> pure [] let everythingPrint = case everything of Just x -> ["\npackage ", "*", "\n ", "ghc-options: ", x, "\n"] Nothing -> [] let pkgSpecificPrint = M.foldrWithKey (\k a b -> [ "\npackage ", T.pack . unPackageName . pkgName . unPkgId $ k , "\n " , "ghc-options: " , a , "\n" ] <> b) [] packagesGhcOpts pure (everythingPrint <> localsPrint <> pkgSpecificPrint) genProject :: Stack -> Resolver -> Project genProject stack Resolver { compiler, deps } = Project (fromMaybe (Ghc "ghc") compiler) (localDirs stack `appendList` localDeps deps) (sortOn repo $ nubOrd $ mapMaybe pickGit deps) (ghcOptions stack) where pickGit :: Dep -> Maybe Git pickGit (Hackage _) = Nothing pickGit (LocalDep _) = Nothing pickGit (SourceDep g) = Just g localDeps :: [Dep] -> [FilePath] localDeps = mapMaybe fromLocalDeps fromLocalDeps :: Dep -> Maybe FilePath fromLocalDeps (Hackage _) = Nothing fromLocalDeps (SourceDep _) = Nothing fromLocalDeps (LocalDep d) = Just d appendList :: NonEmpty a -> [a] -> NonEmpty a appendList (x :| xs) ys = x :| (xs ++ ys) printFreeze :: Freeze -> Text printFreeze (Freeze constraints) = T.concat ["constraints: ", printConstraints, "\n"] where spacing :: Text spacing = ",\n " printConstraints :: Text printConstraints = T.intercalate spacing . fmap printConstraint $ constraints printConstraint :: Constraint -> Text printConstraint (VersionPin pkg) = let name = (T.pack . unPackageName . pkgName $ pkg) ver = (T.pack . prettyShow . pkgVersion $ pkg) in T.concat ["any.", name, " ==", ver] printConstraint (FlagSetting name flags) = T.concat [name, " ", custom flags] custom :: M.Map Text Bool -> Text custom (M.toList -> lst) = T.intercalate " " (renderFlag <$> lst) renderFlag :: (Text, Bool) -> Text renderFlag (name, True) = "+" <> name renderFlag (name, False) = "-" <> name genFreeze :: Resolver -> [PackageName] -- ^ ignore these (local packages) -> Freeze genFreeze Resolver { deps, flags } ignore = let pkgs = filter noSelfs $ unPkgId <$> mapMaybe pick deps uniqpkgs = nubOrdOn pkgName pkgs in Freeze (toConstraints uniqpkgs flags) where pick :: Dep -> Maybe PkgId pick (Hackage p) = Just p pick (SourceDep _) = Nothing pick (LocalDep _) = Nothing noSelfs :: PackageIdentifier -> Bool noSelfs (pkgName -> n) = n `notElem` ignore toConstraints :: [PackageIdentifier] -> Flags -> [Constraint] toConstraints deps' (Flags flags') = let cdeps = fmap VersionPin deps' cflags = M.elems $ M.mapWithKey FlagSetting flags' in sort (cdeps ++ cflags) -- | Acquire all package identifiers from a list of subdirs -- of a git repository. getRemotePkg :: Git -> Bool -> IO [PackageIdentifier] getRemotePkg git@(Git (T.unpack -> repo) (T.unpack -> commit) (fmap T.unpack -> subdirs)) runHpack = withSystemTempDirectory "stack2cabal" $ \dir -> handleIOError (\_ -> hPutStrLn stderr ("Warning: failed to resolve remote .cabal files of: " <> show git) >> pure [] ) $ do callProcess "git" ["clone", repo, dir] callProcess "git" ["-C", dir, "reset", "--hard", commit] case subdirs of [] -> do when runHpack $ do b <- doesFileExist (hpackInput dir) when b $ void $ execHpack dir (Just pid) <- getPackageIdent dir pure [pid] _ -> forM subdirs $ \subdir -> do let fullDir = dir subdir when runHpack $ do b <- doesFileExist (hpackInput fullDir) when b $ void $ execHpack fullDir (Just pid) <- getPackageIdent fullDir pure pid where callProcess :: FilePath -> [String] -> IO () callProcess cmd args = do exit_code <- withCreateProcess (proc cmd args) { delegate_ctlc = True , std_out = UseHandle stderr } $ \_ _ _ p -> waitForProcess p case exit_code of ExitSuccess -> return () ExitFailure r -> throwIO . userError $ ("Process \"" <> cmd <> "\" failed with: " <> show r) -- | Get package identifier from project directory. getPackageIdent :: FilePath -- ^ absolute path to project repository -> IO (Maybe PackageIdentifier) getPackageIdent dir = handleIOError (\_ -> hPutStrLn stderr ("Warning: failed to resolve .cabal file in " <> dir) >> pure Nothing ) $ do cabalFile <- headMay <$> getDirectoryFiles dir ["*.cabal"] forM cabalFile $ \f -> package . packageDescription <$> readGenericPackageDescription silent (dir f) -- | Get all remote VCS packages. getRemotePkgs :: [Git] -> Bool -> IO [PackageIdentifier] getRemotePkgs srcs runHpack = fmap concat $ forM srcs $ \src -> getRemotePkg src runHpack