From fa3fdb3bd4168d1a3ca3e9d01c7bfd7def835faf Mon Sep 17 00:00:00 2001 From: Josh Brown Date: Sat, 2 Jul 2022 22:18:42 +1000 Subject: [PATCH] Support foreign library stanza --- CHANGELOG.md | 2 + README.md | 12 +++++ src/Hpack/Config.hs | 108 +++++++++++++++++++++++++++++++++++++++++-- src/Hpack/Render.hs | 26 +++++++++++ test/EndToEndSpec.hs | 24 ++++++++++ 5 files changed, 169 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dc87e207..471bc29d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ ## Unreleased - Add support for `extra-files` (see #603) + - Add new section: `foreign-libraries`, for Cabal 2.0's `foreign-library` + stanzas (see #518) ## Changes in 0.38.0 - Generate `build-tool-depends` instead of `build-tools` starting with diff --git a/README.md b/README.md index 2fd305bb..f228c258 100644 --- a/README.md +++ b/README.md @@ -151,6 +151,7 @@ library: | `custom-setup` | · | | See [Custom setup](#custom-setup) | | | | `flags` | `flag ` | | Map from flag name to flag (see [Flags](#flags)) | | | | `library` | · | | See [Library fields](#library-fields) | | | +| `foreign-libraries` | `foreign-library ` | | Map from foreign library name to a dict of [Foreign library fields](#foreign-library-fields) and global top-level fields. | | UNRELEASED | | `internal-libraries` | `library ` | | Map from internal library name to a dict of [library fields](#library-fields) and global top-level fields. | | `0.21.0` | | `executables` | `executable ` | | Map from executable name to executable (see [Executable fields](#executable-fields)) | | | | `executable` | `executable ` | | Shortcut for `executables: { package-name: ... }` | | `0.18.0` | @@ -328,6 +329,17 @@ This is done to allow compatibility with a wider range of `Cabal` versions. | `reexported-modules` | · | | | | `signatures` | · | | | +#### Foreign library fields + +| Hpack | Cabal | Default | Notes | +| --- | --- | --- | --- | +| `type` | . | | | +| `lib-version-info` | . | | | +| `options` | . | | | +| `mod-def-file` | . | | | +| `other-modules` | · | All modules in `source-dirs` less `main` less any modules mentioned in `when` | | +| `generated-other-modules` | | | Added to `other-modules` and `autogen-modules`. Since `0.23.0`. + #### Executable fields | Hpack | Cabal | Default | Notes | diff --git a/src/Hpack/Config.hs b/src/Hpack/Config.hs index 8177e812..63e6e9b1 100644 --- a/src/Hpack/Config.hs +++ b/src/Hpack/Config.hs @@ -61,6 +61,7 @@ module Hpack.Config ( , Section(..) , Library(..) , Executable(..) +, ForeignLibrary(..) , Conditional(..) , Cond(..) , Flag(..) @@ -167,6 +168,7 @@ package name version = Package { , packageCustomSetup = Nothing , packageLibrary = Nothing , packageInternalLibraries = mempty + , packageForeignLibraries = mempty , packageExecutables = mempty , packageTests = mempty , packageBenchmarks = mempty @@ -176,6 +178,7 @@ package name version = Package { renamePackage :: String -> Package -> Package renamePackage name p@Package{..} = p { packageName = name + , packageForeignLibraries = fmap (renameDependencies packageName name) packageForeignLibraries , packageExecutables = fmap (renameDependencies packageName name) packageExecutables , packageTests = fmap (renameDependencies packageName name) packageTests , packageBenchmarks = fmap (renameDependencies packageName name) packageBenchmarks @@ -194,6 +197,7 @@ renameDependencies old new sect@Section{..} = sect {sectionDependencies = (Depen packageDependencies :: Package -> [(String, DependencyInfo)] packageDependencies Package{..} = nub . sortBy (comparing (lexicographically . fst)) $ (concatMap deps packageExecutables) + ++ (concatMap deps packageForeignLibraries) ++ (concatMap deps packageTests) ++ (concatMap deps packageBenchmarks) ++ maybe [] deps packageLibrary @@ -237,6 +241,29 @@ instance Semigroup LibrarySection where , librarySectionSignatures = librarySectionSignatures a <> librarySectionSignatures b } +data ForeignLibrarySection = ForeignLibrarySection { + foreignLibrarySectionType :: Last String +, foreignLibrarySectionLibVersionInfo :: Last String +, foreignLibrarySectionOptions :: Maybe (List String) +, foreignLibrarySectionModDefFile :: Last String +, foreignLibrarySectionOtherModules :: Maybe (List Module) +, foreignLibrarySectionGeneratedOtherModules :: Maybe (List Module) +} deriving (Eq, Show, Generic, FromValue) + +instance Monoid ForeignLibrarySection where + mempty = ForeignLibrarySection mempty mempty Nothing mempty Nothing Nothing + mappend = (<>) + +instance Semigroup ForeignLibrarySection where + a <> b = ForeignLibrarySection { + foreignLibrarySectionType = foreignLibrarySectionType a <> foreignLibrarySectionType b + , foreignLibrarySectionLibVersionInfo = foreignLibrarySectionLibVersionInfo a <> foreignLibrarySectionLibVersionInfo b + , foreignLibrarySectionOptions = foreignLibrarySectionOptions a <> foreignLibrarySectionOptions b + , foreignLibrarySectionModDefFile = foreignLibrarySectionModDefFile a <> foreignLibrarySectionModDefFile b + , foreignLibrarySectionOtherModules = foreignLibrarySectionOtherModules a <> foreignLibrarySectionOtherModules b + , foreignLibrarySectionGeneratedOtherModules = foreignLibrarySectionGeneratedOtherModules a <> foreignLibrarySectionGeneratedOtherModules b + } + data ExecutableSection = ExecutableSection { executableSectionMain :: Alias 'True "main-is" (Last FilePath) , executableSectionOtherModules :: Maybe (List Module) @@ -570,10 +597,12 @@ type SectionConfigWithDefaults asmSources cSources cxxSources jsSources a = Prod type PackageConfigWithDefaults asmSources cSources cxxSources jsSources = PackageConfig_ (SectionConfigWithDefaults asmSources cSources cxxSources jsSources LibrarySection) + (SectionConfigWithDefaults asmSources cSources cxxSources jsSources ForeignLibrarySection) (SectionConfigWithDefaults asmSources cSources cxxSources jsSources ExecutableSection) type PackageConfig asmSources cSources cxxSources jsSources = PackageConfig_ (WithCommonOptions asmSources cSources cxxSources jsSources LibrarySection) + (WithCommonOptions asmSources cSources cxxSources jsSources ForeignLibrarySection) (WithCommonOptions asmSources cSources cxxSources jsSources ExecutableSection) data PackageVersion = PackageVersion {unPackageVersion :: String} @@ -584,7 +613,7 @@ instance FromValue PackageVersion where String s -> return (T.unpack s) _ -> typeMismatch "Number or String" v -data PackageConfig_ library executable = PackageConfig { +data PackageConfig_ library foreignLib executable = PackageConfig { packageConfigName :: Maybe String , packageConfigVersion :: Maybe PackageVersion , packageConfigSynopsis :: Maybe String @@ -611,6 +640,8 @@ data PackageConfig_ library executable = PackageConfig { , packageConfigCustomSetup :: Maybe CustomSetupSection , packageConfigLibrary :: Maybe library , packageConfigInternalLibraries :: Maybe (Map String library) +, packageConfigForeignLibraries :: Maybe (Map String foreignLib) +, packageConfigForeignLibrary :: Maybe foreignLib , packageConfigExecutable :: Maybe executable , packageConfigExecutables :: Maybe (Map String executable) , packageConfigTests :: Maybe (Map String executable) @@ -639,6 +670,8 @@ traversePackageConfig :: Traversal PackageConfig traversePackageConfig t p@PackageConfig{..} = do library <- traverse (traverseWithCommonOptions t) packageConfigLibrary internalLibraries <- traverseNamedConfigs t packageConfigInternalLibraries + foreignLibrary <- traverse (traverseWithCommonOptions t) packageConfigForeignLibrary + foreignLibraries <- traverseNamedConfigs t packageConfigForeignLibraries executable <- traverse (traverseWithCommonOptions t) packageConfigExecutable executables <- traverseNamedConfigs t packageConfigExecutables tests <- traverseNamedConfigs t packageConfigTests @@ -646,6 +679,8 @@ traversePackageConfig t p@PackageConfig{..} = do return p { packageConfigLibrary = library , packageConfigInternalLibraries = internalLibraries + , packageConfigForeignLibrary = foreignLibrary + , packageConfigForeignLibraries = foreignLibraries , packageConfigExecutable = executable , packageConfigExecutables = executables , packageConfigTests = tests @@ -739,6 +774,7 @@ addPathsModuleToGeneratedModules pkg | otherwise = pkg { packageLibrary = fmap mapLibrary <$> packageLibrary pkg , packageInternalLibraries = fmap mapLibrary <$> packageInternalLibraries pkg + , packageForeignLibraries = fmap mapForeignLibrary <$> packageForeignLibraries pkg , packageExecutables = fmap mapExecutable <$> packageExecutables pkg , packageTests = fmap mapExecutable <$> packageTests pkg , packageBenchmarks = fmap mapExecutable <$> packageBenchmarks pkg @@ -755,6 +791,15 @@ addPathsModuleToGeneratedModules pkg where generatedModules = libraryGeneratedModules lib + mapForeignLibrary :: ForeignLibrary -> ForeignLibrary + mapForeignLibrary foreignLibrary + | pathsModule `elem` foreignLibraryOtherModules foreignLibrary = foreignLibrary { + foreignLibraryGeneratedModules = if pathsModule `elem` generatedModules then generatedModules else pathsModule : generatedModules + } + | otherwise = foreignLibrary + where + generatedModules = foreignLibraryGeneratedModules foreignLibrary + mapExecutable :: Executable -> Executable mapExecutable executable | pathsModule `elem` executableOtherModules executable = executable { @@ -836,6 +881,7 @@ ensureRequiredCabalVersion inferredLicense pkg@Package{..} = pkg { , makeVersion [3,14] <$ guard (not (null packageExtraFiles)) , packageLibrary >>= libraryCabalVersion , internalLibsCabalVersion packageInternalLibraries + , foreignLibsCabalVersion packageForeignLibraries , executablesCabalVersion packageExecutables , executablesCabalVersion packageTests , executablesCabalVersion packageBenchmarks @@ -859,6 +905,15 @@ ensureRequiredCabalVersion inferredLicense pkg@Package{..} = pkg { where versions = libraryCabalVersion <$> Map.elems internalLibraries + foreignLibsCabalVersion :: Map String (Section ForeignLibrary) -> Maybe CabalVersion + foreignLibsCabalVersion = foldr max Nothing . map foreignLibCabalVersion . Map.elems + + foreignLibCabalVersion :: Section ForeignLibrary -> Maybe CabalVersion + foreignLibCabalVersion sect = maximum [ + makeVersion [2,0] <$ guard (foreignLibraryHasGeneratedModules sect) + , sectionCabalVersion (concatMap getForeignLibraryModules) sect + ] + executablesCabalVersion :: Map String (Section Executable) -> Maybe CabalVersion executablesCabalVersion = foldr max Nothing . map executableCabalVersion . Map.elems @@ -868,6 +923,9 @@ ensureRequiredCabalVersion inferredLicense pkg@Package{..} = pkg { , sectionCabalVersion (concatMap getExecutableModules) sect ] + foreignLibraryHasGeneratedModules :: Section ForeignLibrary -> Bool + foreignLibraryHasGeneratedModules = any (not . null . foreignLibraryGeneratedModules) + executableHasGeneratedModules :: Section Executable -> Bool executableHasGeneratedModules = any (not . null . executableGeneratedModules) @@ -1034,6 +1092,7 @@ data Package = Package { , packageCustomSetup :: Maybe CustomSetup , packageLibrary :: Maybe (Section Library) , packageInternalLibraries :: Map String (Section Library) +, packageForeignLibraries :: Map String (Section ForeignLibrary) , packageExecutables :: Map String (Section Executable) , packageTests :: Map String (Section Executable) , packageBenchmarks :: Map String (Section Executable) @@ -1054,6 +1113,15 @@ data Library = Library { , librarySignatures :: [String] } deriving (Eq, Show) +data ForeignLibrary = ForeignLibrary { + foreignLibraryType :: Maybe String +, foreignLibraryLibVersionInfo :: Maybe String +, foreignLibraryOptions :: Maybe [String] +, foreignLibraryModDefFile :: Maybe String +, foreignLibraryOtherModules :: [Module] +, foreignLibraryGeneratedModules :: [Module] +} deriving (Eq, Show) + data Executable = Executable { executableMain :: Maybe FilePath , executableOtherModules :: [Module] @@ -1177,6 +1245,8 @@ expandSectionDefaults expandSectionDefaults formatYamlParseError userDataDir dir p@PackageConfig{..} = do library <- traverse (expandDefaults formatYamlParseError userDataDir dir) packageConfigLibrary internalLibraries <- traverse (traverse (expandDefaults formatYamlParseError userDataDir dir)) packageConfigInternalLibraries + foreignLibrary <- traverse (expandDefaults formatYamlParseError userDataDir dir) packageConfigForeignLibrary + foreignLibraries <- traverse (traverse (expandDefaults formatYamlParseError userDataDir dir)) packageConfigForeignLibraries executable <- traverse (expandDefaults formatYamlParseError userDataDir dir) packageConfigExecutable executables <- traverse (traverse (expandDefaults formatYamlParseError userDataDir dir)) packageConfigExecutables tests <- traverse (traverse (expandDefaults formatYamlParseError userDataDir dir)) packageConfigTests @@ -1184,6 +1254,8 @@ expandSectionDefaults formatYamlParseError userDataDir dir p@PackageConfig{..} = return p{ packageConfigLibrary = library , packageConfigInternalLibraries = internalLibraries + , packageConfigForeignLibrary = foreignLibrary + , packageConfigForeignLibraries = foreignLibraries , packageConfigExecutable = executable , packageConfigExecutables = executables , packageConfigTests = tests @@ -1241,25 +1313,28 @@ type GlobalOptions = CommonOptions AsmSources CSources CxxSources JsSources Empt toPackage_ :: (MonadIO m, Warnings m, State m) => FilePath -> Product GlobalOptions (PackageConfig AsmSources CSources CxxSources JsSources) -> m Package toPackage_ dir (Product g PackageConfig{..}) = do + foreignLibraryMap <- toExecutableMap packageName packageConfigForeignLibraries packageConfigForeignLibrary executableMap <- toExecutableMap packageName packageConfigExecutables packageConfigExecutable let globalVerbatim = commonOptionsVerbatim g globalOptions = g {commonOptionsVerbatim = Nothing} - executableNames = maybe [] Map.keys executableMap + componentNames = maybe [] Map.keys executableMap ++ maybe [] Map.keys foreignLibraryMap toSect :: (Warnings m, Monoid a) => WithCommonOptions AsmSources CSources CxxSources JsSources a -> m (Section a) - toSect = toSection packageName executableNames . first ((mempty <$ globalOptions) <>) + toSect = toSection packageName componentNames . first ((mempty <$ globalOptions) <>) toSections :: (Warnings m, Monoid a) => Maybe (Map String (WithCommonOptions AsmSources CSources CxxSources JsSources a)) -> m (Map String (Section a)) toSections = maybe (return mempty) (traverse toSect) toLib = toLibrary dir packageName + toForeignLibraries = toSections >=> traverse (toForeignLibrary dir packageName) toExecutables = toSections >=> traverse (toExecutable dir packageName) mLibrary <- traverse (toSect >=> toLib) packageConfigLibrary internalLibraries <- toSections packageConfigInternalLibraries >>= traverse toLib + foreignLibraries <- toForeignLibraries foreignLibraryMap executables <- toExecutables executableMap tests <- toExecutables packageConfigTests benchmarks <- toExecutables packageConfigBenchmarks @@ -1269,6 +1344,7 @@ toPackage_ dir (Product g PackageConfig{..}) = do missingSourceDirs <- liftIO $ nub . sort <$> filterM (fmap not <$> doesDirectoryExist . (dir )) ( maybe [] sectionSourceDirs mLibrary ++ concatMap sectionSourceDirs internalLibraries + ++ concatMap sectionSourceDirs foreignLibraries ++ concatMap sectionSourceDirs executables ++ concatMap sectionSourceDirs tests ++ concatMap sectionSourceDirs benchmarks @@ -1328,6 +1404,7 @@ toPackage_ dir (Product g PackageConfig{..}) = do , packageCustomSetup = mCustomSetup , packageLibrary = mLibrary , packageInternalLibraries = internalLibraries + , packageForeignLibraries = foreignLibraries , packageExecutables = executables , packageTests = tests , packageBenchmarks = benchmarks @@ -1437,6 +1514,9 @@ getMentionedLibraryModules (LibrarySection _ _ exposedModules generatedExposedMo getLibraryModules :: Library -> [Module] getLibraryModules Library{..} = libraryExposedModules ++ libraryOtherModules +getForeignLibraryModules :: ForeignLibrary -> [Module] +getForeignLibraryModules ForeignLibrary{..} = foreignLibraryOtherModules + getExecutableModules :: Executable -> [Module] getExecutableModules Executable{..} = executableOtherModules @@ -1522,6 +1602,28 @@ fromLibrarySectionPlain LibrarySection{..} = Library { , librarySignatures = fromMaybeList librarySectionSignatures } +getMentionedForeignLibraryModules :: ForeignLibrarySection -> [Module] +getMentionedForeignLibraryModules (ForeignLibrarySection _ _ _ _ otherModules generatedModules)= + fromMaybeList (otherModules <> generatedModules) + +toForeignLibrary :: (MonadIO m, State m) => FilePath -> String -> Section ForeignLibrarySection -> m (Section ForeignLibrary) +toForeignLibrary dir packageName_ = + inferModules dir packageName_ getMentionedForeignLibraryModules getForeignLibraryModules fromForeignLibrarySection (fromForeignLibrarySection []) + where + fromForeignLibrarySection :: [Module] -> [Module] -> ForeignLibrarySection -> ForeignLibrary + fromForeignLibrarySection pathsModule inferableModules ForeignLibrarySection{..} = + (ForeignLibrary + (getLast foreignLibrarySectionType) + (getLast foreignLibrarySectionLibVersionInfo) + (fromList <$> foreignLibrarySectionOptions) + (getLast foreignLibrarySectionModDefFile) + (otherModules ++ generatedModules) + generatedModules + ) + where + otherModules = maybe (inferableModules ++ pathsModule) fromList foreignLibrarySectionOtherModules + generatedModules = maybe [] fromList foreignLibrarySectionGeneratedOtherModules + getMentionedExecutableModules :: ExecutableSection -> [Module] getMentionedExecutableModules (ExecutableSection (Alias (Last main)) otherModules generatedModules)= maybe id (:) (toModule . Path.fromFilePath <$> main) $ fromMaybeList (otherModules <> generatedModules) diff --git a/src/Hpack/Render.hs b/src/Hpack/Render.hs index 1cdae985..9a4a5460 100644 --- a/src/Hpack/Render.hs +++ b/src/Hpack/Render.hs @@ -98,6 +98,7 @@ renderPackageWith settings headerFieldsAlignment existingFieldOrder sectionsFiel stanzas = flip runReader (RenderEnv packageCabalVersion packageName) $ do library <- maybe (return []) (fmap return . renderLibrary) packageLibrary internalLibraries <- renderInternalLibraries packageInternalLibraries + foreignLibraries <- renderForeignLibraries packageForeignLibraries executables <- renderExecutables packageExecutables tests <- renderTests packageTests benchmarks <- renderBenchmarks packageBenchmarks @@ -107,6 +108,7 @@ renderPackageWith settings headerFieldsAlignment existingFieldOrder sectionsFiel , map renderFlag packageFlags , library , internalLibraries + , foreignLibraries , executables , tests , benchmarks @@ -181,6 +183,13 @@ renderInternalLibrary :: (String, Section Library) -> RenderM Element renderInternalLibrary (name, sect) = do Stanza ("library " ++ name) <$> renderLibrarySection sect +renderForeignLibraries :: Map String (Section ForeignLibrary) -> RenderM [Element] +renderForeignLibraries = traverse renderForeignLibrary . Map.toList + +renderForeignLibrary :: (String, Section ForeignLibrary) -> RenderM Element +renderForeignLibrary (name, sect) = + Stanza ("foreign-library " ++ name) <$> (renderForeignLibrarySection [] sect) + renderExecutables :: Map String (Section Executable) -> RenderM [Element] renderExecutables = traverse renderExecutable . Map.toList @@ -214,6 +223,20 @@ renderExecutableFields Executable{..} = mainIs ++ [otherModules, generatedModule otherModules = renderOtherModules executableOtherModules generatedModules = renderGeneratedModules executableGeneratedModules +renderForeignLibrarySection :: [Element] -> Section ForeignLibrary -> RenderM [Element] +renderForeignLibrarySection extraFields = renderSection renderForeignLibraryFields extraFields + +renderForeignLibraryFields :: ForeignLibrary -> [Element] +renderForeignLibraryFields ForeignLibrary{..} = + typeField ++ libVersionInfo ++ options ++ modDefFile ++ [otherModules, generatedModules] + where + typeField = maybe [] (return . Field "type" . Literal) foreignLibraryType + libVersionInfo = maybe [] (return . Field "lib-version-info" . Literal) foreignLibraryLibVersionInfo + options = maybe [] (\opts -> [renderForeignLibOptions opts]) foreignLibraryOptions + modDefFile = maybe [] (return . Field "mod-def-file" . Literal) foreignLibraryModDefFile + otherModules = renderOtherModules foreignLibraryOtherModules + generatedModules = renderGeneratedModules foreignLibraryGeneratedModules + renderCustomSetup :: CustomSetup -> Element renderCustomSetup CustomSetup{..} = Stanza "custom-setup" $ renderDependencies "setup-depends" customSetupDependencies @@ -329,6 +352,9 @@ renderDirectories name = Field name . LineSeparatedList . replaceDots "." -> "./" _ -> xs +renderForeignLibOptions :: [String] -> Element +renderForeignLibOptions = Field "options" . LineSeparatedList + renderExposedModules :: [Module] -> Element renderExposedModules = Field "exposed-modules" . LineSeparatedList . map unModule diff --git a/test/EndToEndSpec.hs b/test/EndToEndSpec.hs index 41cbeded..e588452a 100644 --- a/test/EndToEndSpec.hs +++ b/test/EndToEndSpec.hs @@ -1696,6 +1696,19 @@ spec = around_ (inTempDirectoryNamed "my-package") $ do windows |] + describe "foreign libraries" $ do + it "Foreign Library stanza with type and options" $ do + [i| + foreign-library: + type: native-shared + options: + - standalone + |] `shouldRenderTo` (foreignLibrary "my-package" [i| + type: native-shared + options: + standalone + |]) + describe "executables" $ do it "accepts main-is as an alias for main" $ do [i| @@ -2099,6 +2112,17 @@ library #{name} default-language: Haskell2010 |] +foreignLibrary :: String -> String -> Package +foreignLibrary name e = package content + where + content = [i| +foreign-library #{name} + other-modules: + Paths_my_package +#{indentBy 2 $ unindent e} + default-language: Haskell2010 +|] + executable_ :: String -> String -> Package executable_ name e = package content where