{-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE ViewPatterns #-} {-# LANGUAGE CPP #-} module Hpack.Render ( -- | /__NOTE:__/ This module is exposed to allow integration of Hpack into -- other tools. It is not meant for general use by end users. The following -- caveats apply: -- -- * The API is undocumented, consult the source instead. -- -- * The exposed types and functions primarily serve Hpack's own needs, not -- that of a public API. Breaking changes can happen as Hpack evolves. -- -- As an Hpack user you either want to use the @hpack@ executable or a build -- tool that supports Hpack (e.g. @stack@ or @cabal2nix@). renderPackage , renderPackageWith , defaultRenderSettings , RenderSettings(..) , Alignment(..) , CommaStyle(..) #ifdef TEST , renderConditional , renderDependencies , renderLibraryFields , renderExecutableFields , renderFlag , renderSourceRepository , renderDirectories , formatDescription #endif ) where import Imports import Data.Char import Data.Maybe import Data.Map.Lazy (Map) import qualified Data.Map.Lazy as Map import Hpack.Util import Hpack.Config import Hpack.Render.Hints import Hpack.Render.Dsl hiding (sortFieldsBy) import qualified Hpack.Render.Dsl as Dsl renderPackage :: [String] -> Package -> String renderPackage oldCabalFile = renderPackageWith settings alignment formattingHintsFieldOrder formattingHintsSectionsFieldOrder where FormattingHints{..} = sniffFormattingHints oldCabalFile alignment = fromMaybe 16 formattingHintsAlignment settings = formattingHintsRenderSettings renderPackageWith :: RenderSettings -> Alignment -> [String] -> [(String, [String])] -> Package -> String renderPackageWith settings headerFieldsAlignment existingFieldOrder sectionsFieldOrder Package{..} = intercalate "\n" (unlines header : chunks) where chunks :: [String] chunks = map unlines . filter (not . null) . map (render settings 0) $ sortStanzaFields sectionsFieldOrder stanzas header :: [String] header = concatMap (render settings {renderSettingsFieldAlignment = headerFieldsAlignment} 0) packageFields packageFields :: [Element] packageFields = addVerbatim packageVerbatim . sortFieldsBy existingFieldOrder $ headerFields ++ [ Field "tested-with" $ CommaSeparatedList packageTestedWith , Field "extra-source-files" (renderPaths packageExtraSourceFiles) , Field "extra-doc-files" (renderPaths packageExtraDocFiles) , Field "data-files" (renderPaths packageDataFiles) ] ++ maybe [] (return . Field "data-dir" . Literal) packageDataDir sourceRepository :: [Element] sourceRepository = maybe [] (return . renderSourceRepository) packageSourceRepository customSetup :: [Element] customSetup = maybe [] (return . renderCustomSetup) packageCustomSetup library :: [Element] library = maybe [] (return . renderLibrary) packageLibrary stanzas :: [Element] stanzas = concat [ sourceRepository , customSetup , map renderFlag packageFlags , library , renderInternalLibraries packageInternalLibraries , renderExecutables packageExecutables , renderTests packageTests , renderBenchmarks packageBenchmarks ] headerFields :: [Element] headerFields = mapMaybe (\(name, value) -> Field name . Literal <$> value) $ [ ("name", Just packageName) , ("version", Just packageVersion) , ("synopsis", packageSynopsis) , ("description", (formatDescription headerFieldsAlignment <$> packageDescription)) , ("category", packageCategory) , ("stability", packageStability) , ("homepage", packageHomepage) , ("bug-reports", packageBugReports) , ("author", formatList packageAuthor) , ("maintainer", formatList packageMaintainer) , ("copyright", formatList packageCopyright) , ("license", packageLicense) , case packageLicenseFile of [file] -> ("license-file", Just file) files -> ("license-files", formatList files) , ("build-type", Just (show packageBuildType)) ] formatList :: [String] -> Maybe String formatList xs = guard (not $ null xs) >> (Just $ intercalate separator xs) where separator = let Alignment n = headerFieldsAlignment in ",\n" ++ replicate n ' ' sortStanzaFields :: [(String, [String])] -> [Element] -> [Element] sortStanzaFields sectionsFieldOrder = go where go sections = case sections of [] -> [] Stanza name fields : xs | Just fieldOrder <- lookup name sectionsFieldOrder -> Stanza name (sortFieldsBy fieldOrder fields) : go xs x : xs -> x : go xs formatDescription :: Alignment -> String -> String formatDescription (Alignment alignment) description = case map emptyLineToDot $ lines description of x : xs -> intercalate "\n" (x : map (indentation ++) xs) [] -> "" where n = max alignment (length ("description: " :: String)) indentation = replicate n ' ' emptyLineToDot xs | isEmptyLine xs = "." | otherwise = xs isEmptyLine = all isSpace renderSourceRepository :: SourceRepository -> Element renderSourceRepository SourceRepository{..} = Stanza "source-repository head" [ Field "type" "git" , Field "location" (Literal sourceRepositoryUrl) , Field "subdir" (maybe "" Literal sourceRepositorySubdir) ] renderFlag :: Flag -> Element renderFlag Flag {..} = Stanza ("flag " ++ flagName) $ description ++ [ Field "manual" (Literal $ show flagManual) , Field "default" (Literal $ show flagDefault) ] where description = maybe [] (return . Field "description" . Literal) flagDescription renderInternalLibraries :: Map String (Section Library) -> [Element] renderInternalLibraries = map renderInternalLibrary . Map.toList renderInternalLibrary :: (String, Section Library) -> Element renderInternalLibrary (name, sect) = Stanza ("library " ++ name) (renderLibrarySection sect) renderExecutables :: Map String (Section Executable) -> [Element] renderExecutables = map renderExecutable . Map.toList renderExecutable :: (String, Section Executable) -> Element renderExecutable (name, sect) = Stanza ("executable " ++ name) (renderExecutableSection [] sect) renderTests :: Map String (Section Executable) -> [Element] renderTests = map renderTest . Map.toList renderTest :: (String, Section Executable) -> Element renderTest (name, sect) = Stanza ("test-suite " ++ name) (renderExecutableSection [Field "type" "exitcode-stdio-1.0"] sect) renderBenchmarks :: Map String (Section Executable) -> [Element] renderBenchmarks = map renderBenchmark . Map.toList renderBenchmark :: (String, Section Executable) -> Element renderBenchmark (name, sect) = Stanza ("benchmark " ++ name) (renderExecutableSection [Field "type" "exitcode-stdio-1.0"] sect) renderExecutableSection :: [Element] -> Section Executable -> [Element] renderExecutableSection extraFields = renderSection renderExecutableFields extraFields renderExecutableFields :: Executable -> [Element] renderExecutableFields Executable{..} = mainIs ++ [otherModules, generatedModules] where mainIs = maybe [] (return . Field "main-is" . Literal) executableMain otherModules = renderOtherModules executableOtherModules generatedModules = renderGeneratedModules executableGeneratedModules renderCustomSetup :: CustomSetup -> Element renderCustomSetup CustomSetup{..} = Stanza "custom-setup" $ renderDependencies "setup-depends" customSetupDependencies renderLibrary :: Section Library -> Element renderLibrary sect = Stanza "library" $ renderLibrarySection sect renderLibrarySection :: Section Library -> [Element] renderLibrarySection = renderSection renderLibraryFields [] renderLibraryFields :: Library -> [Element] renderLibraryFields Library{..} = maybe [] (return . renderExposed) libraryExposed ++ maybe [] (return . renderVisibility) libraryVisibility ++ [ renderExposedModules libraryExposedModules , renderOtherModules libraryOtherModules , renderGeneratedModules libraryGeneratedModules , renderReexportedModules libraryReexportedModules , renderSignatures librarySignatures ] renderExposed :: Bool -> Element renderExposed = Field "exposed" . Literal . show renderVisibility :: String -> Element renderVisibility = Field "visibility" . Literal renderSection :: (a -> [Element]) -> [Element] -> Section a -> [Element] renderSection renderSectionData extraFieldsStart Section{..} = addVerbatim sectionVerbatim $ extraFieldsStart ++ renderSectionData sectionData ++ [ renderDirectories "hs-source-dirs" sectionSourceDirs , renderDefaultExtensions sectionDefaultExtensions , renderOtherExtensions sectionOtherExtensions , renderGhcOptions sectionGhcOptions , renderGhcProfOptions sectionGhcProfOptions , renderGhcjsOptions sectionGhcjsOptions , renderCppOptions sectionCppOptions , renderCcOptions sectionCcOptions , renderCxxOptions sectionCxxOptions , renderDirectories "include-dirs" sectionIncludeDirs , Field "install-includes" (LineSeparatedList sectionInstallIncludes) , Field "c-sources" (renderPaths sectionCSources) , Field "cxx-sources" (renderPaths sectionCxxSources) , Field "js-sources" (renderPaths sectionJsSources) , renderDirectories "extra-lib-dirs" sectionExtraLibDirs , Field "extra-libraries" (LineSeparatedList sectionExtraLibraries) , renderDirectories "extra-frameworks-dirs" sectionExtraFrameworksDirs , Field "frameworks" (LineSeparatedList sectionFrameworks) , renderLdOptions sectionLdOptions , Field "pkgconfig-depends" (CommaSeparatedList sectionPkgConfigDependencies) ] ++ renderBuildTools sectionBuildTools sectionSystemBuildTools ++ renderDependencies "build-depends" sectionDependencies ++ maybe [] (return . renderBuildable) sectionBuildable ++ maybe [] (return . renderLanguage) sectionLanguage ++ map (renderConditional renderSectionData) sectionConditionals addVerbatim :: [Verbatim] -> [Element] -> [Element] addVerbatim verbatim fields = filterVerbatim verbatim fields ++ renderVerbatim verbatim filterVerbatim :: [Verbatim] -> [Element] -> [Element] filterVerbatim verbatim = filter p where p :: Element -> Bool p = \ case Field name _ -> name `notElem` fields _ -> True fields = concatMap verbatimFieldNames verbatim verbatimFieldNames :: Verbatim -> [String] verbatimFieldNames verbatim = case verbatim of VerbatimLiteral _ -> [] VerbatimObject o -> Map.keys o renderVerbatim :: [Verbatim] -> [Element] renderVerbatim = concatMap $ \ case VerbatimLiteral s -> [Verbatim s] VerbatimObject o -> renderVerbatimObject o renderVerbatimObject :: Map String VerbatimValue -> [Element] renderVerbatimObject = map renderPair . Map.toList where renderPair (key, value) = case lines (verbatimValueToString value) of [x] -> Field key (Literal x) xs -> Field key (LineSeparatedList xs) renderConditional :: (a -> [Element]) -> Conditional (Section a) -> Element renderConditional renderSectionData (Conditional condition sect mElse) = case mElse of Nothing -> if_ Just else_ -> Group if_ (Stanza "else" $ renderSection renderSectionData [] else_) where if_ = Stanza ("if " ++ renderCond condition) (renderSection renderSectionData [] sect) renderCond :: Cond -> String renderCond = \ case CondExpression c -> c CondBool True -> "true" CondBool False -> "false" renderDirectories :: String -> [String] -> Element renderDirectories name = Field name . LineSeparatedList . replaceDots where replaceDots = map replaceDot replaceDot xs = case xs of "." -> "./" _ -> xs renderExposedModules :: [Module] -> Element renderExposedModules = Field "exposed-modules" . LineSeparatedList . map unModule renderOtherModules :: [Module] -> Element renderOtherModules = Field "other-modules" . LineSeparatedList . map unModule renderGeneratedModules :: [Module] -> Element renderGeneratedModules = Field "autogen-modules" . LineSeparatedList . map unModule renderReexportedModules :: [String] -> Element renderReexportedModules = Field "reexported-modules" . CommaSeparatedList renderSignatures :: [String] -> Element renderSignatures = Field "signatures" . CommaSeparatedList renderDependencies :: String -> Dependencies -> [Element] renderDependencies name deps = [ Field name (CommaSeparatedList renderedDeps) , Field "mixins" (CommaSeparatedList $ concat mixins) ] where (renderedDeps, mixins) = unzip . map renderDependency . Map.toList $ unDependencies deps renderDependency :: (String, DependencyInfo) -> (String, [String]) renderDependency (name, DependencyInfo mixins version) = ( name ++ renderVersion version , [ name ++ " " ++ mixin | mixin <- mixins ] ) renderVersion :: DependencyVersion -> String renderVersion (DependencyVersion _ c) = renderVersionConstraint c renderVersionConstraint :: VersionConstraint -> String renderVersionConstraint version = case version of AnyVersion -> "" VersionRange x -> " " ++ x renderBuildTools :: Map BuildTool DependencyVersion -> SystemBuildTools -> [Element] renderBuildTools (map renderBuildTool . Map.toList -> xs) systemBuildTools = [ Field "build-tools" (CommaSeparatedList $ [x | BuildTools x <- xs] ++ renderSystemBuildTools systemBuildTools) , Field "build-tool-depends" (CommaSeparatedList [x | BuildToolDepends x <- xs]) ] data RenderBuildTool = BuildTools String | BuildToolDepends String renderBuildTool :: (BuildTool, DependencyVersion) -> RenderBuildTool renderBuildTool (buildTool, renderVersion -> version) = case buildTool of LocalBuildTool executable -> BuildTools (executable ++ version) BuildTool pkg executable | pkg == executable && executable `elem` knownBuildTools -> BuildTools (executable ++ version) | otherwise -> BuildToolDepends (pkg ++ ":" ++ executable ++ version) where knownBuildTools :: [String] knownBuildTools = [ "alex" , "c2hs" , "cpphs" , "greencard" , "haddock" , "happy" , "hsc2hs" , "hscolour" ] renderSystemBuildTools :: SystemBuildTools -> [String] renderSystemBuildTools = map renderSystemBuildTool . Map.toList . unSystemBuildTools renderSystemBuildTool :: (String, VersionConstraint) -> String renderSystemBuildTool (name, constraint) = name ++ renderVersionConstraint constraint renderLanguage :: Language -> Element renderLanguage (Language lang) = Field "default-language" (Literal lang) renderGhcOptions :: [GhcOption] -> Element renderGhcOptions = Field "ghc-options" . WordList renderGhcProfOptions :: [GhcProfOption] -> Element renderGhcProfOptions = Field "ghc-prof-options" . WordList renderGhcjsOptions :: [GhcjsOption] -> Element renderGhcjsOptions = Field "ghcjs-options" . WordList renderCppOptions :: [CppOption] -> Element renderCppOptions = Field "cpp-options" . WordList renderCcOptions :: [CcOption] -> Element renderCcOptions = Field "cc-options" . WordList renderCxxOptions :: [CxxOption] -> Element renderCxxOptions = Field "cxx-options" . WordList renderLdOptions :: [LdOption] -> Element renderLdOptions = Field "ld-options" . WordList renderBuildable :: Bool -> Element renderBuildable = Field "buildable" . Literal . show renderDefaultExtensions :: [String] -> Element renderDefaultExtensions = Field "default-extensions" . LineSeparatedList renderOtherExtensions :: [String] -> Element renderOtherExtensions = Field "other-extensions" . LineSeparatedList renderPaths :: [Path] -> Value renderPaths = LineSeparatedList . map renderPath where renderPath :: Path -> FilePath renderPath (Path path) | needsQuoting path = show path | otherwise = path needsQuoting :: FilePath -> Bool needsQuoting = any (\x -> isSpace x || x == ',') sortFieldsBy :: [String] -> [Element] -> [Element] sortFieldsBy existingFieldOrder = Dsl.sortFieldsBy ("import" : existingFieldOrder)