From 6c97d52c57316653b37e96e4b941abd29a86a19a Mon Sep 17 00:00:00 2001 From: Josh Brown Date: Sat, 2 Jul 2022 22:18:42 +1000 Subject: [PATCH 1/8] Support foreign library stanza --- CHANGELOG.md | 4 ++ README.md | 12 +++++ src/Hpack/Config.hs | 108 +++++++++++++++++++++++++++++++++++++++++-- src/Hpack/Render.hs | 26 +++++++++++ test/EndToEndSpec.hs | 24 ++++++++++ 5 files changed, 171 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ce04e218..55e8f51f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## Changes in UNRELEASED + - Add new section: `foreign-libraries`, for Cabal 2.0's `foreign-library` + stanzas (see #518) + ## Changes in 0.40.0 - Infer `cabal-version: 3.4` (rather than `cabal-version: 3.0`) when a section has sublibraries as dependencies. Make the same inference when custom setup diff --git a/README.md b/README.md index 881b4bfd..9650d641 100644 --- a/README.md +++ b/README.md @@ -153,6 +153,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` | @@ -331,6 +332,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 9faf6365..3c5e39c0 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) @@ -573,10 +600,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} @@ -587,7 +616,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 @@ -615,6 +644,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) @@ -643,6 +674,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 @@ -650,6 +683,8 @@ traversePackageConfig t p@PackageConfig{..} = do return p { packageConfigLibrary = library , packageConfigInternalLibraries = internalLibraries + , packageConfigForeignLibrary = foreignLibrary + , packageConfigForeignLibraries = foreignLibraries , packageConfigExecutable = executable , packageConfigExecutables = executables , packageConfigTests = tests @@ -743,6 +778,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 @@ -759,6 +795,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 { @@ -841,6 +886,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 @@ -882,6 +928,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 @@ -892,6 +947,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) @@ -1062,6 +1120,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) @@ -1082,6 +1141,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] @@ -1206,6 +1274,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 @@ -1213,6 +1283,8 @@ expandSectionDefaults formatYamlParseError userDataDir dir p@PackageConfig{..} = return p{ packageConfigLibrary = library , packageConfigInternalLibraries = internalLibraries + , packageConfigForeignLibrary = foreignLibrary + , packageConfigForeignLibraries = foreignLibraries , packageConfigExecutable = executable , packageConfigExecutables = executables , packageConfigTests = tests @@ -1270,25 +1342,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 @@ -1298,6 +1373,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 @@ -1357,6 +1433,7 @@ toPackage_ dir (Product g PackageConfig{..}) = do , packageCustomSetup = mCustomSetup , packageLibrary = mLibrary , packageInternalLibraries = internalLibraries + , packageForeignLibraries = foreignLibraries , packageExecutables = executables , packageTests = tests , packageBenchmarks = benchmarks @@ -1474,6 +1551,9 @@ getMentionedLibraryModules (LibrarySection _ _ exposedModules generatedExposedMo getLibraryModules :: Library -> [Module] getLibraryModules Library{..} = libraryExposedModules ++ libraryOtherModules +getForeignLibraryModules :: ForeignLibrary -> [Module] +getForeignLibraryModules ForeignLibrary{..} = foreignLibraryOtherModules + getExecutableModules :: Executable -> [Module] getExecutableModules Executable{..} = executableOtherModules @@ -1559,6 +1639,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 2ede8d5c..b7123237 100644 --- a/src/Hpack/Render.hs +++ b/src/Hpack/Render.hs @@ -102,6 +102,7 @@ renderPackageWith RenderSettings{..} headerFieldsAlignment existingFieldOrder se 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 @@ -111,6 +112,7 @@ renderPackageWith RenderSettings{..} headerFieldsAlignment existingFieldOrder se , map renderFlag packageFlags , library , internalLibraries + , foreignLibraries , executables , tests , benchmarks @@ -175,6 +177,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 @@ -208,6 +217,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 @@ -324,6 +347,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 d6121416..205a4cb0 100644 --- a/test/EndToEndSpec.hs +++ b/test/EndToEndSpec.hs @@ -1822,6 +1822,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| @@ -2225,6 +2238,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 From fd7969883ff9a89c6ea1c2b738a0f3eaa996d1ed Mon Sep 17 00:00:00 2001 From: Mike Pilgrem Date: Mon, 8 Jun 2026 20:24:47 +0100 Subject: [PATCH 2/8] Revert removal of executableNames --- src/Hpack/Config.hs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Hpack/Config.hs b/src/Hpack/Config.hs index 3c5e39c0..549d827a 100644 --- a/src/Hpack/Config.hs +++ b/src/Hpack/Config.hs @@ -1348,10 +1348,10 @@ toPackage_ dir (Product g PackageConfig{..}) = do globalVerbatim = commonOptionsVerbatim g globalOptions = g {commonOptionsVerbatim = Nothing} - componentNames = maybe [] Map.keys executableMap ++ maybe [] Map.keys foreignLibraryMap + executableNames = maybe [] Map.keys executableMap toSect :: (Warnings m, Monoid a) => WithCommonOptions AsmSources CSources CxxSources JsSources a -> m (Section a) - toSect = toSection packageName componentNames . first ((mempty <$ globalOptions) <>) + toSect = toSection packageName executableNames . 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) From 953051a974e39690146ce347305d58579e517a7a Mon Sep 17 00:00:00 2001 From: Mike Pilgrem Date: Mon, 8 Jun 2026 23:49:57 +0100 Subject: [PATCH 3/8] Foreign-libraries: remove type, options and mod-def-file keys Also does not respect legacy Paths_ module handling. --- README.md | 19 +++++----- src/Hpack/Config.hs | 87 ++++++++++++++++++++++++++++++++++----------- src/Hpack/Render.hs | 18 ++++++++-- 3 files changed, 89 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index 9650d641..dc07928c 100644 --- a/README.md +++ b/README.md @@ -332,17 +332,6 @@ 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 | @@ -369,6 +358,14 @@ This is done to allow compatibility with a wider range of `Cabal` versions. | `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`. +#### Foreign library fields + +| Hpack | Cabal | Default | Notes | +| --- | --- | --- | --- | +| `lib-version-info` | . | | | +| `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`. + #### Flags | Hpack | Cabal | Default | Notes | diff --git a/src/Hpack/Config.hs b/src/Hpack/Config.hs index 549d827a..0796c288 100644 --- a/src/Hpack/Config.hs +++ b/src/Hpack/Config.hs @@ -59,9 +59,11 @@ module Hpack.Config ( , verbatimValueToString , CustomSetup(..) , Section(..) +, emptySectionForeignLibrary , Library(..) , Executable(..) , ForeignLibrary(..) +, emptyForeignLibrary , Conditional(..) , Cond(..) , Flag(..) @@ -242,24 +244,18 @@ instance Semigroup LibrarySection where } data ForeignLibrarySection = ForeignLibrarySection { - foreignLibrarySectionType :: Last String -, foreignLibrarySectionLibVersionInfo :: Last String -, foreignLibrarySectionOptions :: Maybe (List String) -, foreignLibrarySectionModDefFile :: Last String + foreignLibrarySectionLibVersionInfo :: 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 + mempty = ForeignLibrarySection 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 + foreignLibrarySectionLibVersionInfo = foreignLibrarySectionLibVersionInfo a <> foreignLibrarySectionLibVersionInfo b , foreignLibrarySectionOtherModules = foreignLibrarySectionOtherModules a <> foreignLibrarySectionOtherModules b , foreignLibrarySectionGeneratedOtherModules = foreignLibrarySectionGeneratedOtherModules a <> foreignLibrarySectionGeneratedOtherModules b } @@ -1142,14 +1138,23 @@ data Library = Library { } deriving (Eq, Show) data ForeignLibrary = ForeignLibrary { + -- This is a hack. The 'type' field is required but we also want to exclude it + -- from: + -- + -- if os(windows) + -- options: standalone + -- + -- So, we treat it as if it were optional: foreignLibraryType :: Maybe String -, foreignLibraryLibVersionInfo :: Maybe String , foreignLibraryOptions :: Maybe [String] -, foreignLibraryModDefFile :: Maybe String +, foreignLibraryLibVersionInfo :: Maybe String , foreignLibraryOtherModules :: [Module] , foreignLibraryGeneratedModules :: [Module] } deriving (Eq, Show) +emptyForeignLibrary :: ForeignLibrary +emptyForeignLibrary = ForeignLibrary Nothing Nothing Nothing [] [] + data Executable = Executable { executableMain :: Maybe FilePath , executableOtherModules :: [Module] @@ -1194,6 +1199,42 @@ data Section a = Section { , sectionVerbatim :: [Verbatim] } deriving (Eq, Show, Functor, Foldable, Traversable) +emptySectionForeignLibrary :: Section ForeignLibrary +emptySectionForeignLibrary = Section { + sectionData = emptyForeignLibrary + , sectionSourceDirs = mempty + , sectionDependencies = mempty + , sectionPkgConfigDependencies = mempty + , sectionDefaultExtensions = mempty + , sectionOtherExtensions = mempty + , sectionLanguage = Nothing + , sectionMhsOptions = mempty + , sectionGhcOptions = mempty + , sectionGhcProfOptions = mempty + , sectionGhcSharedOptions = mempty + , sectionGhcjsOptions = mempty + , sectionCppOptions = mempty + , sectionAsmOptions = mempty + , sectionAsmSources = mempty + , sectionCcOptions = mempty + , sectionCSources = mempty + , sectionCxxOptions = mempty + , sectionCxxSources = mempty + , sectionJsSources = mempty + , sectionExtraLibDirs = mempty + , sectionExtraLibraries = mempty + , sectionExtraFrameworksDirs = mempty + , sectionFrameworks = mempty + , sectionIncludeDirs = mempty + , sectionInstallIncludes = mempty + , sectionLdOptions = mempty + , sectionBuildable = Nothing + , sectionConditionals = mempty + , sectionBuildTools = mempty + , sectionSystemBuildTools = mempty + , sectionVerbatim = mempty + } + data Conditional a = Conditional { conditionalCondition :: Cond , conditionalThen :: a @@ -1568,7 +1609,9 @@ removeConditionalsThatAreAlwaysFalse sect = sect { p = (/= CondBool False) . conditionalCondition inferModules :: (MonadIO m, State m) => - FilePath + Bool + -- ^ Support only the modern handling of Paths_ modules? + -> FilePath -> String -> (a -> [Module]) -> (b -> [Module]) @@ -1576,13 +1619,15 @@ inferModules :: (MonadIO m, State m) => -> ([Module] -> a -> b) -> Section a -> m (Section b) -inferModules dir packageName_ getMentionedModules getInferredModules fromData fromConditionals sect_ = do +inferModules modernPaths dir packageName_ getMentionedModules getInferredModules fromData fromConditionals sect_ = do specVersion <- State.get let pathsModule :: [Module] pathsModule = case specVersion of SpecVersion v | v >= Version.makeVersion [0,36,0] -> [] - _ -> [pathsModuleFromPackageName packageName_] + _ + | modernPaths -> [] + | otherwise -> [pathsModuleFromPackageName packageName_] removeConditionalsThatAreAlwaysFalse <$> traverseSectionAndConditionals (fromConfigSection fromData pathsModule) @@ -1601,7 +1646,7 @@ inferModules dir packageName_ getMentionedModules getInferredModules fromData fr toLibrary :: (MonadIO m, State m) => FilePath -> String -> Section LibrarySection -> m (Section Library) toLibrary dir name = - inferModules dir name getMentionedLibraryModules getLibraryModules fromLibrarySectionTopLevel fromLibrarySectionInConditional + inferModules False dir name getMentionedLibraryModules getLibraryModules fromLibrarySectionTopLevel fromLibrarySectionInConditional where fromLibrarySectionTopLevel :: [Module] -> [Module] -> LibrarySection -> Library fromLibrarySectionTopLevel pathsModule inferableModules LibrarySection{..} = @@ -1640,20 +1685,20 @@ fromLibrarySectionPlain LibrarySection{..} = Library { } getMentionedForeignLibraryModules :: ForeignLibrarySection -> [Module] -getMentionedForeignLibraryModules (ForeignLibrarySection _ _ _ _ otherModules generatedModules)= +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 []) + inferModules True dir packageName_ getMentionedForeignLibraryModules getForeignLibraryModules fromForeignLibrarySection (fromForeignLibrarySection []) where fromForeignLibrarySection :: [Module] -> [Module] -> ForeignLibrarySection -> ForeignLibrary fromForeignLibrarySection pathsModule inferableModules ForeignLibrarySection{..} = (ForeignLibrary - (getLast foreignLibrarySectionType) + -- Cabal accepts native-static or native-shared but supports only the latter: + (Just "native-shared") + Nothing (getLast foreignLibrarySectionLibVersionInfo) - (fromList <$> foreignLibrarySectionOptions) - (getLast foreignLibrarySectionModDefFile) (otherModules ++ generatedModules) generatedModules ) @@ -1667,7 +1712,7 @@ getMentionedExecutableModules (ExecutableSection (Alias (Last main)) otherModule toExecutable :: (MonadIO m, State m) => FilePath -> String -> Section ExecutableSection -> m (Section Executable) toExecutable dir packageName_ = - inferModules dir packageName_ getMentionedExecutableModules getExecutableModules fromExecutableSection (fromExecutableSection []) + inferModules False dir packageName_ getMentionedExecutableModules getExecutableModules fromExecutableSection (fromExecutableSection []) . expandMain where fromExecutableSection :: [Module] -> [Module] -> ExecutableSection -> Executable diff --git a/src/Hpack/Render.hs b/src/Hpack/Render.hs index b7123237..4dd539c5 100644 --- a/src/Hpack/Render.hs +++ b/src/Hpack/Render.hs @@ -182,7 +182,20 @@ renderForeignLibraries = traverse renderForeignLibrary . Map.toList renderForeignLibrary :: (String, Section ForeignLibrary) -> RenderM Element renderForeignLibrary (name, sect) = - Stanza ("foreign-library " ++ name) <$> (renderForeignLibrarySection [] sect) + Stanza ("foreign-library " ++ name) <$> (renderForeignLibrarySection [] sect') + where + -- Cabal supports 'options: standalone' only and requires it to be set on + -- Windows and to be not set on all other operating systems: + sect' = + sect { sectionConditionals = conditionalOptions : sectionConditionals sect } + conditionalOptions = Conditional { + conditionalCondition = CondExpression "os(windows)" + , conditionalThen = + emptySectionForeignLibrary { sectionData = optionsStandalone } + , conditionalElse = Nothing + } + optionsStandalone = + emptyForeignLibrary { foreignLibraryOptions = Just ["standalone"] } renderExecutables :: Map String (Section Executable) -> RenderM [Element] renderExecutables = traverse renderExecutable . Map.toList @@ -222,12 +235,11 @@ renderForeignLibrarySection extraFields = renderSection renderForeignLibraryFiel renderForeignLibraryFields :: ForeignLibrary -> [Element] renderForeignLibraryFields ForeignLibrary{..} = - typeField ++ libVersionInfo ++ options ++ modDefFile ++ [otherModules, generatedModules] + typeField ++ libVersionInfo ++ options ++ [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 From afa0561e3586b2f55fa2304be3cd6d43a80cd7dc Mon Sep 17 00:00:00 2001 From: Mike Pilgrem Date: Mon, 8 Jun 2026 20:48:00 +0100 Subject: [PATCH 4/8] foreign-libraries: Revise test and add test --- test/EndToEndSpec.hs | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/test/EndToEndSpec.hs b/test/EndToEndSpec.hs index 205a4cb0..c2515360 100644 --- a/test/EndToEndSpec.hs +++ b/test/EndToEndSpec.hs @@ -1825,14 +1825,25 @@ spec = around_ (inTempDirectoryNamed "my-package") $ do describe "foreign libraries" $ do it "Foreign Library stanza with type and options" $ do [i| - foreign-library: - type: native-shared + foreign-libraries: + my-library: {} + |] `shouldRenderTo` (foreignLibrary "my-library" [i| + type: native-shared + default-language: Haskell2010 + if os(windows) options: - - standalone + standalone + |]) + + it "Foreign Library stanza named after package" $ do + [i| + foreign-library: {} |] `shouldRenderTo` (foreignLibrary "my-package" [i| type: native-shared - options: - standalone + default-language: Haskell2010 + if os(windows) + options: + standalone |]) describe "executables" $ do @@ -2243,10 +2254,7 @@ 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 From 4e4c71002fbb4bc465a50423000cc6a9a52b2add Mon Sep 17 00:00:00 2001 From: Mike Pilgrem Date: Mon, 8 Jun 2026 19:05:44 +0100 Subject: [PATCH 5/8] Relocate foreign-libraries in README.md This weakens a possible implication that libraries provided by a Haskell package and foreign-libraries are somehow related. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index dc07928c..030d757e 100644 --- a/README.md +++ b/README.md @@ -153,12 +153,12 @@ 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` | | `tests` | `test-suite ` | | Map from test name to test (see [Test fields](#test-fields)) | | | | `benchmarks` | `benchmark ` | | Map from benchmark name to benchmark (see [Benchmark fields](#benchmark-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 | | `defaults` | | | See [Defaults](#defaults), may be a list | | | #### cabal-version From 902433771fbcdf99b3d46804bac424d388b24ffe Mon Sep 17 00:00:00 2001 From: Mike Pilgrem Date: Mon, 8 Jun 2026 19:41:24 +0100 Subject: [PATCH 6/8] Add foreign-library to README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 030d757e..0f1b719c 100644 --- a/README.md +++ b/README.md @@ -159,6 +159,7 @@ library: | `tests` | `test-suite ` | | Map from test name to test (see [Test fields](#test-fields)) | | | | `benchmarks` | `benchmark ` | | Map from benchmark name to benchmark (see [Benchmark fields](#benchmark-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 | +| `foreign-library` | `foreign-library ` | | Shortcut for `foreign-libraries: { package-name: ... } | | UNRELEASED | | `defaults` | | | See [Defaults](#defaults), may be a list | | | #### cabal-version From 65251297c65510443487f01615d3df77e4afca6c Mon Sep 17 00:00:00 2001 From: Mike Pilgrem Date: Mon, 8 Jun 2026 19:45:33 +0100 Subject: [PATCH 7/8] In README.md, conform foreign-libraries to executables --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0f1b719c..3c44a8ab 100644 --- a/README.md +++ b/README.md @@ -158,8 +158,8 @@ library: | `executable` | `executable ` | | Shortcut for `executables: { package-name: ... }` | | `0.18.0` | | `tests` | `test-suite ` | | Map from test name to test (see [Test fields](#test-fields)) | | | | `benchmarks` | `benchmark ` | | Map from benchmark name to benchmark (see [Benchmark fields](#benchmark-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 | -| `foreign-library` | `foreign-library ` | | Shortcut for `foreign-libraries: { package-name: ... } | | UNRELEASED | +| `foreign-libraries` | `foreign-library ` | | Map from foreign library name to foreign library (see [Foreign library fields](#foreign-library-fields)) | | UNRELEASED | +| `foreign-library` | `foreign-library ` | | Shortcut for `foreign-libraries: { package-name: ... }` | | UNRELEASED | | `defaults` | | | See [Defaults](#defaults), may be a list | | | #### cabal-version From e3a78aaa807f04ce96764955cc9699e31d6b62ca Mon Sep 17 00:00:00 2001 From: Mike Pilgrem Date: Mon, 8 Jun 2026 20:11:12 +0100 Subject: [PATCH 8/8] Foreign-library: Fix README.md for other-modules and generated-other-modules --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3c44a8ab..54c831ce 100644 --- a/README.md +++ b/README.md @@ -364,8 +364,8 @@ This is done to allow compatibility with a wider range of `Cabal` versions. | Hpack | Cabal | Default | Notes | | --- | --- | --- | --- | | `lib-version-info` | . | | | -| `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`. +| `other-modules` | · | All modules in `source-dirs` less any modules mentioned in `when` | | +| `generated-other-modules` | | | Added to `other-modules` and `autogen-modules`.| #### Flags