diff options
| author | Hannes Schulze <haschu0103@outlook.de> | 2025-06-20 16:27:42 +0200 |
|---|---|---|
| committer | Hannes Schulze <haschu0103@outlook.de> | 2025-06-20 16:27:42 +0200 |
| commit | 519956797d905c0925b8dd418ceb17c7495ad30a (patch) | |
| tree | 5a26d1b6df1ddce517a47d3105761820b29a8953 | |
| parent | 1af3d14f3f995645a0de08a54b244c19840709e9 (diff) | |
set up haskell bindings for c lib
| -rw-r--r-- | app/Main.hs | 29 | ||||
| -rw-r--r-- | app/foobar.cabal (renamed from foobar.cabal) | 8 | ||||
| -rw-r--r-- | cabal.project | 2 | ||||
| -rw-r--r-- | flake.nix | 12 | ||||
| -rw-r--r-- | hie.yaml | 18 | ||||
| -rw-r--r-- | lib/buildgen.cabal | 31 | ||||
| -rw-r--r-- | lib/buildgen.hs | 268 | ||||
| -rw-r--r-- | lib/cabal.project | 1 | ||||
| -rw-r--r-- | lib/dbus/com.github.hannesschulze.foobar.xml (renamed from csrc/dbus/com.github.hannesschulze.foobar.xml) | 0 | ||||
| -rw-r--r-- | lib/dbus/org.bluez.xml (renamed from csrc/dbus/org.bluez.xml) | 0 | ||||
| -rw-r--r-- | lib/dbus/org.freedesktop.Notifications.xml (renamed from csrc/dbus/org.freedesktop.Notifications.xml) | 0 | ||||
| -rw-r--r-- | lib/dbus/org.freedesktop.UPower.xml (renamed from csrc/dbus/org.freedesktop.UPower.xml) | 0 | ||||
| -rw-r--r-- | lib/hs/.gitignore | 3 | ||||
| -rw-r--r-- | lib/hs/Foobar.overrides | 0 | ||||
| -rw-r--r-- | lib/hs/pkg.info | 16 | ||||
| -rw-r--r-- | lib/meson.build | 90 | ||||
| -rw-r--r-- | meson.build | 2 | ||||
| -rw-r--r-- | readme.md | 10 | ||||
| -rwxr-xr-x | setup-lib.sh | 17 | ||||
| -rw-r--r-- | src/Main.hs | 2 |
20 files changed, 496 insertions, 13 deletions
diff --git a/app/Main.hs b/app/Main.hs new file mode 100644 index 0000000..609b53d --- /dev/null +++ b/app/Main.hs @@ -0,0 +1,29 @@ +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE OverloadedLabels #-} +{-# LANGUAGE OverloadedRecordDot #-} +{-# LANGUAGE ImplicitParams #-} + +import Control.Monad (void) +import Data.GI.Base +import qualified GI.Gtk as Gtk +import qualified GI.Gio as Gio +import qualified GI.Foobar as FB + +activate :: Gtk.Application -> IO () +activate app = do + button <- new Gtk.Button [ #label := "Hello, world!" ] + window <- new Gtk.ApplicationWindow [ #application := app + , #title := "Test" + , #child := button ] + _ <- FB.serverProxyNewForBusSync Gio.BusTypeSession + [] + "com.github.hannesschulze.foobar" + "/com/github/hannesschulze/foobar/server" + (Nothing :: Maybe Gio.Cancellable) + window.show + +main :: IO () +main = do + app <- new Gtk.Application [ #applicationId := "com.github.hannesschulze.foobar-hs" + , On #activate (activate ?self) ] + void $ app.run Nothing diff --git a/foobar.cabal b/app/foobar.cabal index d3eea46..7dcebc6 100644 --- a/foobar.cabal +++ b/app/foobar.cabal @@ -9,7 +9,11 @@ common warnings executable foobar import: warnings - hs-source-dirs: src main-is: Main.hs - build-depends: base ^>=4.19.2.0 + build-depends: base, + haskell-gi, + haskell-gi-base, + gi-gio, + gi-gtk4, + gi-foobar default-language: Haskell2010 diff --git a/cabal.project b/cabal.project new file mode 100644 index 0000000..1d0b610 --- /dev/null +++ b/cabal.project @@ -0,0 +1,2 @@ +packages: lib/hs/gi-foobar.cabal + app/foobar.cabal @@ -15,10 +15,18 @@ let pkgs = import nixpkgs { inherit system; }; haskell = pkgs.haskellPackages.ghcWithPackages (p: - with p; [ haskell-language-server ]); + with p; [ haskell-language-server haskell-gi-base gi-gtk4 ]); haskell-shell = pkgs.mkShell { name = "Haskell"; - packages = [ haskell pkgs.gdb pkgs.pkg-config pkgs.cabal-install ]; + packages = [ + haskell + pkgs.gdb + pkgs.pkg-config + pkgs.cabal-install + pkgs.meson + pkgs.ninja + pkgs.gobject-introspection + ]; hardeningDisable = [ "fortify" ]; }; foobar = (pkgs.callPackage ./nix/package.nix {}); diff --git a/hie.yaml b/hie.yaml new file mode 100644 index 0000000..a5c8943 --- /dev/null +++ b/hie.yaml @@ -0,0 +1,18 @@ +cradle: + multi: + - path: "lib" + config: + cradle: + cabal: + cabalProject: "lib/cabal.project" + components: + - path: "lib" + component: "exe:buildgen" + - path: "." + config: + cradle: + cabal: + - path: "app" + component: "exe:foobar" + - path: "lib/hs" + component: "lib:gi-foobar" diff --git a/lib/buildgen.cabal b/lib/buildgen.cabal new file mode 100644 index 0000000..b7789c5 --- /dev/null +++ b/lib/buildgen.cabal @@ -0,0 +1,31 @@ +name: build-tools +version: 0.0.1 +synopsis: Utils for generating bindings, for internal use +description: Utils for generating bindings, for internal use +homepage: https://github.com/haskell-gi/haskell-gi +license: LGPL-2.1 + -- or above +author: Iñaki García Etxebarria +maintainer: Iñaki García Etxebarria (garetxe@gmail.com) +stability: Experimental +category: Development +build-type: Simple +cabal-version: 2.0 + +executable buildgen + main-is: buildgen.hs + default-language: Haskell2010 + default-extensions: CPP + + build-depends: base >= 4.7 && < 5, + haskell-gi >= 0.20, + text >= 1.2, + filepath >= 1.4, + bytestring >= 0.10, + directory >= 1.3, + containers, + aeson, + aeson-pretty, + process ^>= 1.6.0 + + ghc-options: -Wall diff --git a/lib/buildgen.hs b/lib/buildgen.hs new file mode 100644 index 0000000..6cbdf0b --- /dev/null +++ b/lib/buildgen.hs @@ -0,0 +1,268 @@ +{-# LANGUAGE OverloadedStrings, DeriveGeneric #-} + +-- Based on: +-- https://raw.githubusercontent.com/haskell-gi/haskell-gi/refs/heads/master/bindings/genBuildInfo.hs + +-- Generate the cabal info for the given subdirectories, assuming +-- the existence of appropriate "pkg.info" files. + +import System.Directory (createDirectory, doesPathExist, makeAbsolute) +import System.Environment (getArgs) +import System.FilePath ((</>), (<.>), takeBaseName, takeDirectory) +import System.IO (hPutStrLn, stderr, hFlush, stdout) +import System.Exit (exitFailure) + +#if !MIN_VERSION_base(4,13,0) +import Data.Monoid ((<>)) +#endif + +import GHC.Generics +import qualified Data.Aeson as A +import qualified Data.ByteString as B +import Data.Maybe (fromMaybe, fromJust) +import qualified Data.Set as S +import qualified Data.Text.Encoding as TE +import qualified Data.Text as T +import Data.Text (Text) + +import Data.GI.CodeGen.CabalHooks (configureDryRun) +import qualified Data.GI.CodeGen.ProjectInfo as PI +import Data.GI.CodeGen.Util (ucFirst) + +import qualified Data.ByteString.Lazy as LB + +data ProjectInfo = ProjectInfo { + name :: T.Text -- ^ Name of the haskell package + , version :: T.Text -- ^ Its version + , author :: Maybe T.Text -- ^ Auhor. If not specified the + -- current maintainer (from + -- Data.GI.CodeGen.ProjectInfo) + -- will be used. + , synopsis :: T.Text + , description :: T.Text + , giDepends :: [T.Text] -- ^ dependencies on other + -- haskell-gi generated modules. + , baseVersion :: T.Text -- ^ Constraint on base for this module. + + , girName :: T.Text -- ^ GIR file (without version) + , girVersion :: T.Text -- ^ Its version + , girOverrides :: Maybe FilePath -- ^ Possibly an overrides file + , compat :: Maybe T.Text + } deriving (Show, Generic) + +instance A.FromJSON ProjectInfo +instance A.ToJSON ProjectInfo + +readGIRInfo :: FilePath -> IO ProjectInfo +readGIRInfo fname = do + buf <- LB.readFile fname + case A.eitherDecode buf of + Left err -> error ("Could not parse \"" <> fname <> "\": " <> err) + Right info -> return info + +writeCabal :: FilePath -> FilePath -> ProjectInfo -> [Text] -> IO () +writeCabal fname lib info exposed = + B.writeFile fname $ TE.encodeUtf8 $ T.unlines $ + [ "name: " <> name info + , "version: " <> version info + , "synopsis: " <> synopsis info + , "description: " <> description info + , "homepage: " <> PI.homepage + , "license: " <> PI.license + , "author: " <> fromMaybe PI.maintainers (author info) + , "maintainer: " <> PI.maintainers + , "category: " <> PI.category + , "build-type: Custom" + , "cabal-version: 2.0" + , let commonFiles = "README.md ChangeLog.md stack.yaml" + in case girOverrides info of + Nothing -> "\nextra-source-files: " <> commonFiles <> "\n" + Just ov -> "\nextra-source-files: " <> commonFiles <> " " + <> T.pack ov <> "\n" + , "custom-setup" + , " setup-depends: " <> + T.intercalate ",\n " + ([ "base >= 4.11 && < 5" + , "Cabal >= 1.24 && < 4" + , "haskell-gi >= 0.26.14 && < 0.27"] + <> giDepends info) + , "" + , "library" + , " default-language: " <> PI.defaultLanguage + , " default-extensions: " <> T.intercalate ", " PI.defaultExtensions + , " other-extensions: " <> T.intercalate ", " PI.otherExtensions + , " ghc-options: " <> T.intercalate " " PI.ghcOptions + , "" + , " extra-lib-dirs: " <> T.pack (takeDirectory lib) + , " extra-libraries: " <> fromJust (T.stripPrefix "lib" $ T.pack $ takeBaseName lib) + , " build-depends: " <> + T.intercalate ",\n " + ([ baseVersion info + , "haskell-gi-base >= 0.26 && < 0.27" + -- Workaround for cabal new-build not picking up + -- setup-depends dependencies when constructing the + -- build plan. + , "haskell-gi >= 0.26.14 && < 0.27" + -- See https://github.com/haskell-gi/haskell-gi/issues/124 + -- for the reasoning behind this. + , "haskell-gi-overloading < 1.1" ] + <> giDepends info <> PI.standardDeps) + , "" + -- GHC 8.2.x panics when building the overloaded bindings + -- https://ghc.haskell.org/trac/ghc/ticket/14382 + , " -- Disable overloading when compiling under GHC 8.2.x" + , " -- see https://ghc.haskell.org/trac/ghc/ticket/14382" + , " if impl(ghc == 8.2.*)" + , " build-depends: haskell-gi-overloading == 0.0" + , "" + , " -- Note that the following list of exposed modules and autogen" + , " -- modules is for documentation purposes only, so that some" + , " -- documentation appears in hackage. The actual list of modules" + , " -- to be built will be built at configure time, based on the" + , " -- available introspection data." + , "" + , " exposed-modules: " <> + T.intercalate ",\n " exposed + , "" + , " autogen-modules: " <> + T.intercalate ",\n " exposed + ] + +writeSetup :: FilePath -> ProjectInfo -> S.Set Text -> IO () +writeSetup fname info deps = + B.writeFile fname $ TE.encodeUtf8 $ T.unlines + [ "{-# LANGUAGE OverloadedStrings #-}" + , "" + , "import Data.GI.CodeGen.CabalHooks (setupBinding, TaggedOverride(..))" + , "" + , T.unlines (map buildInfo (S.toList deps)) + , "" + , "main :: IO ()" + , "main = setupBinding name version pkgName pkgVersion verbose overridesFile inheritedOverrides outputDir" + , " where name = " <> tshow (girName info) + , " version = " <> tshow (girVersion info) + , " pkgName = " <> tshow (name info) + , " pkgVersion = " <> tshow (version info) + , " overridesFile = " <> tshow (girOverrides info) + , " verbose = False" + , " outputDir = Nothing" + , " inheritedOverrides = [" + <> T.intercalate ", " (map inheritedOverride (S.toList deps)) + <> "]" + ] + where tshow :: Show a => a -> Text + tshow = T.pack . show + + buildInfo :: Text -> Text + buildInfo dep = let capDep = ucFirst dep in + "import qualified GI." <> capDep <> ".Config as " <> capDep + + inheritedOverride :: Text -> Text + inheritedOverride dep = let capDep = ucFirst dep in + "TaggedOverride \"inherited:" <> capDep <> "\" " + <> capDep <> ".overrides" + +exposedModulesAndDeps :: FilePath -> ProjectInfo -> IO ([Text], S.Set Text) +exposedModulesAndDeps dir info = + configureDryRun (girName info) (girVersion info) (name info) (version info) + ((dir </>) <$> girOverrides info) [] + +-- | Write a "compat" package re-exporting all symbols from the +-- current pakage. +writeCompatPkg :: FilePath -> Text -> ProjectInfo -> IO () +writeCompatPkg dir compatPkg info = do + alreadyThere <- doesPathExist dir + if alreadyThere + then return () + else do + putStr $ " [+ " <> T.unpack compatPkg <> " ]" + hFlush stdout + createDirectory dir + writeCompatProject (dir </> "cabal.project") compatPkg + writeCompatCabal (dir </> T.unpack compatPkg <.> ".cabal") compatPkg info + writeCompatSetup (dir </> "Setup.hs") info + writeCompatReadme (dir </> "README.md") info + +-- | The .cabal file for the compatibility package. +writeCompatCabal :: FilePath -> Text -> ProjectInfo -> IO () +writeCompatCabal fname compatPkg info = + B.writeFile fname $ TE.encodeUtf8 $ T.unlines + [ "name: " <> compatPkg + , "version: " <> version info + , "synopsis: " <> synopsis info <> " (compatibility layer)" + , "description: This package re-exports (for backward compatibility)" + , " the haskell-gi generated bindings in the " <> name info <> " package." + , "homepage: " <> PI.homepage + , "license: " <> PI.license + , "license-file: LICENSE" + , "author: " <> fromMaybe PI.maintainers (author info) + , "maintainer: " <> PI.maintainers + , "category: " <> PI.category + , "build-type: Custom" + , "cabal-version: 2.0" + , "" + , "extra-source-files: README.md" + , "" + , "custom-setup" + , " setup-depends:" + , " base >= 4.11 && <5," + -- For compatibility with stack, see + -- https://github.com/haskell-gi/haskell-gi/issues/463 + , " Cabal >= 1.24 && < 4," + , " haskell-gi ^>= 0.26.14," + , " " <> name info <> " ^>= " <> version info + , "" + , "library" + , " ghc-options: -Wall" + , "" + , " build-depends: base >= 4.11 && <5," + , " " <> name info <> " ^>= " <> version info + , "" + , " default-language: Haskell2010" + ] + +writeCompatProject :: FilePath -> Text -> IO () +writeCompatProject fname compatPkg = + B.writeFile fname $ TE.encodeUtf8 $ "packages: " <> compatPkg <> ".cabal" + +writeCompatSetup :: FilePath -> ProjectInfo -> IO () +writeCompatSetup fname info = + let cfgMod = "GI." <> ucFirst (girName info) <> ".Config" + in B.writeFile fname $ TE.encodeUtf8 $ T.unlines + [ "import Data.GI.CodeGen.CabalHooks (setupCompatWrapper)" + , "import qualified " <> cfgMod <> " as Cfg" + , "" + , "main :: IO ()" + , "main = setupCompatWrapper \"" <> name info <> "\" Cfg.modules" + ] + +writeCompatReadme :: FilePath -> ProjectInfo -> IO () +writeCompatReadme fname info = + let link = "[" <> name info <> "](/package/" <> name info <> ")" + in B.writeFile fname $ TE.encodeUtf8 $ T.unlines + [ "# Information" + , "This is a compatibility package. For newer projects we recommend that you use " <> link <> " instead." + ] + +genBindingsInDir :: FilePath -> FilePath -> IO () +genBindingsInDir dir lib = do + info <- readGIRInfo (dir </> "pkg.info") + putStr $ "Generating " <> T.unpack (name info) <> "-" <> T.unpack (version info) <> " ..." + hFlush stdout + (exposed, deps) <- exposedModulesAndDeps dir info + writeCabal (dir </> T.unpack (name info) <.> "cabal") lib info exposed + writeSetup (dir </> "Setup.hs") info deps + case compat info of + Nothing -> return () + Just compatPkg -> writeCompatPkg (dir </> "compat") compatPkg info + putStrLn " done." + +main :: IO () +main = do + args <- getArgs + + case args of + [lib, srcdir] -> do absLib <- makeAbsolute lib + genBindingsInDir srcdir absLib + _ -> do hPutStrLn stderr "usage: buildgen <lib> <srcdir>" + exitFailure diff --git a/lib/cabal.project b/lib/cabal.project new file mode 100644 index 0000000..3438af7 --- /dev/null +++ b/lib/cabal.project @@ -0,0 +1 @@ +packages: buildgen.cabal diff --git a/csrc/dbus/com.github.hannesschulze.foobar.xml b/lib/dbus/com.github.hannesschulze.foobar.xml index e8373a6..e8373a6 100644 --- a/csrc/dbus/com.github.hannesschulze.foobar.xml +++ b/lib/dbus/com.github.hannesschulze.foobar.xml diff --git a/csrc/dbus/org.bluez.xml b/lib/dbus/org.bluez.xml index 669e325..669e325 100644 --- a/csrc/dbus/org.bluez.xml +++ b/lib/dbus/org.bluez.xml diff --git a/csrc/dbus/org.freedesktop.Notifications.xml b/lib/dbus/org.freedesktop.Notifications.xml index 6b1d2f2..6b1d2f2 100644 --- a/csrc/dbus/org.freedesktop.Notifications.xml +++ b/lib/dbus/org.freedesktop.Notifications.xml diff --git a/csrc/dbus/org.freedesktop.UPower.xml b/lib/dbus/org.freedesktop.UPower.xml index 18fc41d..18fc41d 100644 --- a/csrc/dbus/org.freedesktop.UPower.xml +++ b/lib/dbus/org.freedesktop.UPower.xml diff --git a/lib/hs/.gitignore b/lib/hs/.gitignore new file mode 100644 index 0000000..988260d --- /dev/null +++ b/lib/hs/.gitignore @@ -0,0 +1,3 @@ +GI/ +*.cabal +Setup.hs diff --git a/lib/hs/Foobar.overrides b/lib/hs/Foobar.overrides new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/lib/hs/Foobar.overrides diff --git a/lib/hs/pkg.info b/lib/hs/pkg.info new file mode 100644 index 0000000..e1171d0 --- /dev/null +++ b/lib/hs/pkg.info @@ -0,0 +1,16 @@ +{ + "name": "gi-foobar", + "version": "1.0.0", + "description": "Bindings for the Foobar base library, autogenerated by haskell-gi.", + "synopsis": "Foobar bindings", + "author": null, + "girName": "Foobar", + "girVersion": "1.0.0", + "girOverrides": "Foobar.overrides", + "baseVersion": "base >= 4.9 && <5", + "giDepends": [ + "gi-glib == 2.0.*", + "gi-gobject == 2.0.*", + "gi-gio == 2.0.*" + ] +} diff --git a/lib/meson.build b/lib/meson.build new file mode 100644 index 0000000..473ac62 --- /dev/null +++ b/lib/meson.build @@ -0,0 +1,90 @@ +project('foobar', 'c', default_options: ['c_std=gnu11', 'warning_level=1']) + +gnome = import('gnome') +glib_dep = dependency('glib-2.0', version: '>=2.78') +gio_dep = dependency('gio-2.0', version: '>=2.78') + +deps = [ + glib_dep, + gio_dep, +] + +sources = [] + +sources += gnome.gdbus_codegen( + 'server', + 'dbus/com.github.hannesschulze.foobar.xml', + namespace: 'Foobar', + annotations: [ + ['com.github.hannesschulze.foobar.Server', 'org.gtk.GDBus.C.Name', 'Server'], + ], +) + +sources += gnome.gdbus_codegen( + 'notifications', + 'dbus/org.freedesktop.Notifications.xml', + namespace: 'Foobar', + annotations: [ + ['org.freedesktop.Notifications', 'org.gtk.GDBus.C.Name', 'Notifications'], + ], +) + +sources += gnome.gdbus_codegen( + 'upower', + 'dbus/org.freedesktop.UPower.xml', + namespace: 'Foobar', + annotations: [ + ['org.freedesktop.UPower.Device', 'org.gtk.GDBus.C.Name', 'UPowerDevice'], + ], +) + +sources += gnome.gdbus_codegen( + 'bluez', + 'dbus/org.bluez.xml', + namespace: 'Foobar', + annotations: [ + ['org.bluez.Adapter1', 'org.gtk.GDBus.C.Name', 'BluezAdapter'], + ['org.bluez.Device1', 'org.gtk.GDBus.C.Name', 'BluezDevice'], + ['org.bluez.AgentManager1', 'org.gtk.GDBus.C.Name', 'BluezAgentManager'], + ], +) + +c_args = [ + '-Wall', + '-Werror', + '-Wswitch-enum', + '-Wswitch-default', + '-Wunused', + '-Wuninitialized', +] + +static_library( + 'foobar_static', + sources, + dependencies: deps, + install: false, + c_args: c_args +) + +libfoobar = shared_library( + 'foobar', + sources, + dependencies: deps, + install: false, + c_args: c_args +) + +libfoobar_gir = gnome.generate_gir( + libfoobar, + sources: sources, + namespace: 'Foobar', + link_with: libfoobar, + nsversion: '1.0.0', + identifier_prefix: 'Foobar', + symbol_prefix: 'foobar', + export_packages: 'foobar', + dependencies: deps, + includes: [ 'GObject-2.0', 'Gio-2.0' ], + install: false, + extra_args: [ '--fallback-library-path=' ] +) diff --git a/meson.build b/meson.build index f1cdb9d..5e75111 100644 --- a/meson.build +++ b/meson.build @@ -2,8 +2,6 @@ project('foobar', 'c', default_options: ['c_std=gnu11', 'warning_level=1']) gnome = import('gnome') -glib_dep = dependency('glib-2.0', version: '>=2.78') -gio_dep = dependency('gio-2.0', version: '>=2.78') gio_unix_dep = dependency('gio-unix-2.0', version: '>=2.78') gtk_dep = dependency('gtk4', version: '>=4.12') json_dep = dependency('json-glib-1.0', version: '>=1.8') @@ -53,18 +53,18 @@ In addition, these dependencies should be available at runtime: ### Building -To manually build foobar, run the following commands: +To set up the build environment, run the following command: ```sh git clone https://github.com/hannesschulze/foobar.git && cd foobar -meson setup build --prefix=/usr -ninja -C build +nix develop +source setup-lib.sh ``` -Then, install it using the following command: +To build foobar, run ```sh -sudo ninja install -C build +cabal build foobar ``` ## Usage diff --git a/setup-lib.sh b/setup-lib.sh new file mode 100755 index 0000000..d827f13 --- /dev/null +++ b/setup-lib.sh @@ -0,0 +1,17 @@ +pushd lib + +rm -rf build +meson setup build +ninja -C build + +cabal clean +LD_LIBRARY_PATH="build:$LD_LIBRARY_PATH" \ +HASKELL_GI_GIR_SEARCH_PATH="build" \ +HASKELL_GI_TYPELIB_SEARCH_PATH="build" \ +cabal run buildgen build/libfoobar_static.a hs + +popd + +export LD_LIBRARY_PATH="$(pwd)/lib/build:$LD_LIBRARY_PATH" +export HASKELL_GI_GIR_SEARCH_PATH="$(pwd)/lib/build" +export HASKELL_GI_TYPELIB_SEARCH_PATH="$(pwd)/lib/build" diff --git a/src/Main.hs b/src/Main.hs deleted file mode 100644 index 3b09145..0000000 --- a/src/Main.hs +++ /dev/null @@ -1,2 +0,0 @@ -main :: IO () -main = putStrLn "Hello, world!" |
