diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c8d48d..89b07db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,9 @@ -# Revision history for arion-compose +# Revision history for Arion -## 0.1.0.0 -- YYYY-mm-dd +## Unreleased -* First version. Released on an unsuspecting world. -* *BREAKING:* useHostStore now uses a proper empty base image, like `scratch`. + + +## 0.1.0.0 -- 2019-10-04 + +* First released version. Released on an unsuspecting world. diff --git a/arion-compose.cabal b/arion-compose.cabal index 2ff2f0b..853bb3a 100644 --- a/arion-compose.cabal +++ b/arion-compose.cabal @@ -1,9 +1,9 @@ cabal-version: 2.4 name: arion-compose -version: 0.1.0.0 +version: 0.1.0.0 synopsis: Run docker-compose with help from Nix/NixOS --- description: +description: Arion is a tool for building and running applications that consist of multiple docker containers using NixOS modules. It has special support for docker images that are built with Nix, for a smooth development experience and improved performance. homepage: https://github.com/hercules-ci/arion#readme -- bug-reports: license: Apache-2.0 @@ -11,10 +11,10 @@ license-file: LICENSE author: Robert Hensing maintainer: robert@hercules-ci.com -- copyright: --- category: -extra-source-files: CHANGELOG.md, README.asciidoc -write-ghc-enviroment-files: - never +category: Distribution, Network, Cloud, Distributed Computing +extra-source-files: CHANGELOG.md, README.asciidoc, + src/haskell/testdata/**/*.nix + src/haskell/testdata/**/*.json data-files: nix/*.nix , nix/modules/composition/*.nix , nix/modules/nixos/*.nix @@ -24,7 +24,7 @@ data-files: nix/*.nix -- all data is verbatim from some sources data-dir: src -common deps +common common build-depends: base ^>=4.12.0.0 , aeson , aeson-pretty @@ -38,25 +38,27 @@ common deps , text , protolude , unix + ghc-options: -Wall flag ghci default: False manual: True library - import: deps + import: common exposed-modules: Arion.Nix Arion.Aeson Arion.DockerCompose Arion.Images Arion.Services other-modules: Paths_arion_compose + autogen-modules: Paths_arion_compose -- other-extensions: hs-source-dirs: src/haskell/lib default-language: Haskell2010 executable arion - import: deps + import: common main-is: Main.hs -- other-modules: -- other-extensions: @@ -66,7 +68,7 @@ executable arion default-language: Haskell2010 test-suite arion-unit-tests - import: deps + import: common if flag(ghci) hs-source-dirs: src/haskell/lib ghc-options: -Wno-missing-home-modules diff --git a/nix/ci.nix b/nix/ci.nix index 91f1a0f..a8bca06 100644 --- a/nix/ci.nix +++ b/nix/ci.nix @@ -1,6 +1,47 @@ -args@{ pkgs ? import ./default.nix args, system ? null, ... }: +let + sources = import ./sources.nix; + lib = import (sources."nixpkgs" + "/lib"); + inherit (import sources."project.nix" { inherit lib; }) dimension; +in -{ - inherit (pkgs) arion tests; - doc = pkgs.recurseIntoAttrs (import ../doc { inherit pkgs; }); -} +dimension "Nixpkgs version" { + "nixos-19_03" = { + nixpkgsSource = "nixpkgs"; + isReferenceNixpkgs = true; + }; + "nixos-19_09" = { + nixpkgsSource = "nixos-19.09"; + + # Broken since 19.09, wontfix because doc tooling will be changed. + # TODO: reenable + enableDoc = false; + }; + "nixos-unstable" = { + nixpkgsSource = "nixos-unstable"; + + # Broken since 19.09, wontfix because doc tooling will be changed. + # TODO: reenable + enableDoc = false; + }; + } ( + _name: { nixpkgsSource, isReferenceNixpkgs ? false, enableDoc ? true }: + + + dimension "System" { + "x86_64-linux" = { isReferenceTarget = isReferenceNixpkgs; }; + # TODO: darwin + # "x86_64-darwin" = { enableNixOSTests = false; }; + } ( + system: { isReferenceTarget ? false }: + let + pkgs = import ./. { inherit system; nixpkgsSrc = sources.${nixpkgsSource}; }; + in + { + inherit (pkgs) arion tests; + } // lib.optionalAttrs enableDoc { + doc = pkgs.recurseIntoAttrs (import ../doc { inherit pkgs; }); + } // lib.optionalAttrs isReferenceTarget { + inherit (pkgs.arion-project.haskellPkgs) arion-compose-checked; + } + ) + ) diff --git a/nix/default.nix b/nix/default.nix index c3fd071..7393d12 100644 --- a/nix/default.nix +++ b/nix/default.nix @@ -1,12 +1,10 @@ -/** - * This is the entry-point for all nix execution in this project. - */ -{ nixpkgsSrc ? ./nixpkgs.nix -, system ? null +{ sources ? import ./sources.nix +, nixpkgsSrc ? sources.nixpkgs +, system ? builtins.currentSystem , ... }: -import (import ./nixpkgs.nix) ({ +import nixpkgsSrc ({ # Makes the config pure as well. See /top-level/impure.nix: config = { }; @@ -14,6 +12,5 @@ import (import ./nixpkgs.nix) ({ # all the packages are defined there: (import ./overlay.nix) ]; -} // (if system == null then {} else { inherit system; -})) +}) diff --git a/nix/haskell-overlay.nix b/nix/haskell-overlay.nix index c2d82cb..7d5ba27 100644 --- a/nix/haskell-overlay.nix +++ b/nix/haskell-overlay.nix @@ -1,4 +1,16 @@ self: super: hself: hsuper: { arion-compose = import ./haskell-arion-compose.nix { pkgs = self; haskellPackages = hself; }; + arion-compose-checked = + let pkg = super.haskell.lib.buildStrictly hself.arion-compose; + checked = super.haskell.lib.overrideCabal pkg (o: { + postConfigure = ''${o.postConfigure or ""} + if ! ${hsuper.cabal-install}/bin/cabal check; + then + echo 1>&2 ERROR: cabal file is invalid. Above warnings were errors. + exit 1 + fi + ''; + }); + in checked; } \ No newline at end of file diff --git a/nix/nixpkgs.nix b/nix/nixpkgs.nix deleted file mode 100644 index 662487c..0000000 --- a/nix/nixpkgs.nix +++ /dev/null @@ -1,5 +0,0 @@ -# to update: $ nix-prefetch-url --unpack url -builtins.fetchTarball { - url = "https://github.com/NixOS/nixpkgs/archive/bd5e8f35c2e9d1ddc9cd2fea7a23563336d54acb.tar.gz"; - sha256 = "1wnzqqijrwf797nb234q10zb1h7086njradkkrx3a15b303grsw4"; -} diff --git a/nix/overlay.nix b/nix/overlay.nix index 8b5490c..163a653 100644 --- a/nix/overlay.nix +++ b/nix/overlay.nix @@ -3,10 +3,12 @@ let inherit (self.arion-project) haskellPkgs; inherit (super) lib; + sources = import ./sources.nix; + in { - arion = import ./arion.nix { pkgs = self; }; + inherit (import ./.. { pkgs = self; }) arion; tests = super.callPackage ../tests {}; doc = super.callPackage ../doc {}; @@ -19,7 +21,12 @@ in haskellPkgs.ghcid super.docker-compose (import ~/h/ghcide-nix {}).ghcide-ghc864 + self.niv + self.releaser ]; }; }; + + inherit (import (sources.niv) {}) niv; + releaser = self.haskellPackages.callCabal2nix "releaser" sources.releaser {}; } diff --git a/nix/sources.json b/nix/sources.json new file mode 100644 index 0000000..7e4b088 --- /dev/null +++ b/nix/sources.json @@ -0,0 +1,75 @@ +{ + "niv": { + "branch": "master", + "description": "Easy dependency management for Nix projects", + "homepage": "https://github.com/nmattia/niv", + "owner": "nmattia", + "repo": "niv", + "rev": "1dd094156b249586b66c16200ecfd365c7428dc0", + "sha256": "1b2vjnn8iac5iiqszjc2v1s1ygh0yri998c0k3s4x4kn0dsqik21", + "type": "tarball", + "url": "https://github.com/nmattia/niv/archive/1dd094156b249586b66c16200ecfd365c7428dc0.tar.gz", + "url_template": "https://github.com///archive/.tar.gz" + }, + "nixos-19.09": { + "branch": "nixos-19.09", + "description": "A read-only mirror of NixOS/nixpkgs tracking the released channels. Send issues and PRs to", + "homepage": "https://github.com/NixOS/nixpkgs", + "owner": "NixOS", + "repo": "nixpkgs-channels", + "rev": "3ba0d9f75ccffd41e32cfea4046805f8bbab12f5", + "sha256": "0w20drs4mwlq12k1sss1x8adyf5ph5jd52n8wdcgmn4sm60qjmki", + "type": "tarball", + "url": "https://github.com/NixOS/nixpkgs-channels/archive/3ba0d9f75ccffd41e32cfea4046805f8bbab12f5.tar.gz", + "url_template": "https://github.com///archive/.tar.gz" + }, + "nixos-unstable": { + "branch": "nixos-unstable", + "description": "A read-only mirror of NixOS/nixpkgs tracking the released channels. Send issues and PRs to", + "homepage": "https://github.com/NixOS/nixpkgs", + "owner": "NixOS", + "repo": "nixpkgs-channels", + "rev": "cb4332e3eb6dfdb653f1fc7397a0292df228a533", + "sha256": "1722wphznqhpfny08rcy19l85r2l893ckjc3h1vfivj6aj64fwjr", + "type": "tarball", + "url": "https://github.com/NixOS/nixpkgs-channels/archive/cb4332e3eb6dfdb653f1fc7397a0292df228a533.tar.gz", + "url_template": "https://github.com///archive/.tar.gz", + "version": "" + }, + "nixpkgs": { + "branch": "nixos-19.03", + "description": "A read-only mirror of NixOS/nixpkgs tracking the released channels. Send issues and PRs to", + "homepage": "https://github.com/NixOS/nixpkgs", + "owner": "NixOS", + "repo": "nixpkgs-channels", + "rev": "6420e2649fa9e267481fb78e602022dab9d1dcd1", + "sha256": "1z3hx7gp8nxk3fspi8vik3j9zxpajj3s7nxvjvx3b5igndxwbp74", + "type": "tarball", + "url": "https://github.com/NixOS/nixpkgs-channels/archive/6420e2649fa9e267481fb78e602022dab9d1dcd1.tar.gz", + "url_template": "https://github.com///archive/.tar.gz" + }, + "project.nix": { + "branch": "master", + "description": "A configuration manager for your projects", + "homepage": null, + "owner": "hercules-ci", + "repo": "project.nix", + "rev": "33e5f3cb25feff4ccd00f8c60a05976e2ee01802", + "sha256": "0c3q3il5h6q3ms8m6da51knvjsfvpz12sh3a3av4d2a5ikm5ncl1", + "type": "tarball", + "url": "https://github.com/hercules-ci/project.nix/archive/33e5f3cb25feff4ccd00f8c60a05976e2ee01802.tar.gz", + "url_template": "https://github.com///archive/.tar.gz" + }, + "releaser": { + "branch": "master", + "description": "Automation of Haskell package release process.", + "homepage": null, + "owner": "domenkozar", + "repo": "releaser", + "rev": "43a4e27654f388e8eacab631e24e26792ff88fe2", + "sha256": "072jlbw0hdc4nvs9frd7wdyzdv4mz2dc5ib35iaqi9rzdafq6822", + "type": "tarball", + "url": "https://github.com/domenkozar/releaser/archive/43a4e27654f388e8eacab631e24e26792ff88fe2.tar.gz", + "url_template": "https://github.com///archive/.tar.gz" + } +} diff --git a/nix/sources.nix b/nix/sources.nix new file mode 100644 index 0000000..d4ac577 --- /dev/null +++ b/nix/sources.nix @@ -0,0 +1,93 @@ +# This file has been generated by Niv. + +# A record, from name to path, of the third-party packages +with rec +{ + pkgs = + if hasNixpkgsPath + then + if hasThisAsNixpkgsPath + then import (builtins_fetchTarball { inherit (sources_nixpkgs) url sha256; }) {} + else import {} + else + import (builtins_fetchTarball { inherit (sources_nixpkgs) url sha256; }) {}; + + sources_nixpkgs = + if builtins.hasAttr "nixpkgs" sources + then sources.nixpkgs + else abort + '' + Please specify either (through -I or NIX_PATH=nixpkgs=...) or + add a package called "nixpkgs" to your sources.json. + ''; + + # fetchTarball version that is compatible between all the versions of Nix + builtins_fetchTarball = + { url, sha256 }@attrs: + let + inherit (builtins) lessThan nixVersion fetchTarball; + in + if lessThan nixVersion "1.12" then + fetchTarball { inherit url; } + else + fetchTarball attrs; + + # fetchurl version that is compatible between all the versions of Nix + builtins_fetchurl = + { url, sha256 }@attrs: + let + inherit (builtins) lessThan nixVersion fetchurl; + in + if lessThan nixVersion "1.12" then + fetchurl { inherit url; } + else + fetchurl attrs; + + # A wrapper around pkgs.fetchzip that has inspectable arguments, + # annoyingly this means we have to specify them + fetchzip = { url, sha256 }@attrs: pkgs.fetchzip attrs; + + # A wrapper around pkgs.fetchurl that has inspectable arguments, + # annoyingly this means we have to specify them + fetchurl = { url, sha256 }@attrs: pkgs.fetchurl attrs; + + hasNixpkgsPath = (builtins.tryEval ).success; + hasThisAsNixpkgsPath = + (builtins.tryEval ).success && == ./.; + + sources = builtins.fromJSON (builtins.readFile ./sources.json); + + mapAttrs = builtins.mapAttrs or + (f: set: with builtins; + listToAttrs (map (attr: { name = attr; value = f attr set.${attr}; }) (attrNames set))); + + # borrowed from nixpkgs + functionArgs = f: f.__functionArgs or (builtins.functionArgs f); + callFunctionWith = autoArgs: f: args: + let auto = builtins.intersectAttrs (functionArgs f) autoArgs; + in f (auto // args); + + getFetcher = spec: + let fetcherName = + if builtins.hasAttr "type" spec + then builtins.getAttr "type" spec + else "builtin-tarball"; + in builtins.getAttr fetcherName { + "tarball" = fetchzip; + "builtin-tarball" = builtins_fetchTarball; + "file" = fetchurl; + "builtin-url" = builtins_fetchurl; + }; +}; +# NOTE: spec must _not_ have an "outPath" attribute +mapAttrs (_: spec: + if builtins.hasAttr "outPath" spec + then abort + "The values in sources.json should not have an 'outPath' attribute" + else + if builtins.hasAttr "url" spec && builtins.hasAttr "sha256" spec + then + spec // + { outPath = callFunctionWith spec (getFetcher spec) { }; } + else spec + ) sources diff --git a/src/haskell/exe/Main.hs b/src/haskell/exe/Main.hs index 57fcada..6f806c6 100644 --- a/src/haskell/exe/Main.hs +++ b/src/haskell/exe/Main.hs @@ -12,20 +12,13 @@ import qualified Arion.DockerCompose as DockerCompose import Arion.Services (getDefaultExec) import Options.Applicative -import Control.Applicative import Control.Monad.Fail -import qualified Data.Aeson.Encode.Pretty import qualified Data.Text as T import qualified Data.Text.IO as T -import qualified Data.Text.Lazy as TL -import qualified Data.Text.Lazy.Builder as TB -import qualified Data.List.NonEmpty as NE import Data.List.NonEmpty (NonEmpty(..)) -import Control.Arrow ((>>>)) - import System.Posix.User (getRealUserID) data CommonOptions = @@ -69,6 +62,7 @@ parseOptions = do let nixArgs = userNixArgs <|> "--show-trace" <$ guard showTrace in CommonOptions{..} +textArgument :: Mod ArgumentFields [Char] -> Parser Text textArgument = fmap T.pack . strArgument parseCommand :: Parser (CommonOptions -> IO ()) @@ -124,18 +118,18 @@ commandDC -> Text -> Text -> Mod CommandFields (CommonOptions -> IO ()) -commandDC run cmdStr help = +commandDC run cmdStr helpText = command (T.unpack cmdStr) (info (run cmdStr <$> parseDockerComposeArgs) - (progDesc (T.unpack help) <> fullDesc <> forwardOptions)) + (progDesc (T.unpack helpText) <> fullDesc <> forwardOptions)) -------------------------------------------------------------------------------- runDC :: Text -> DockerComposeArgs -> CommonOptions -> IO () -runDC cmd (DockerComposeArgs args) opts = do +runDC cmd (DockerComposeArgs args) _opts = do DockerCompose.run DockerCompose.Args { files = [] , otherArgs = [cmd] ++ args @@ -265,7 +259,7 @@ runExec detach privileged user noTTY index envs workDir service commandAndArgs o main :: IO () main = - (join . execParser) (info (parseAll <**> helper) fullDesc) + (join . arionExecParser) (info (parseAll <**> helper) fullDesc) where - execParser = customExecParser (prefs showHelpOnEmpty) + arionExecParser = customExecParser (prefs showHelpOnEmpty) diff --git a/src/haskell/lib/Arion/Aeson.hs b/src/haskell/lib/Arion/Aeson.hs index dd3ae12..02c9b89 100644 --- a/src/haskell/lib/Arion/Aeson.hs +++ b/src/haskell/lib/Arion/Aeson.hs @@ -4,11 +4,9 @@ import Prelude () import Data.Aeson import qualified Data.ByteString.Lazy as BL import qualified Data.Text.Lazy as TL -import qualified Data.Text.Lazy.IO as TL import qualified Data.Text.Lazy.Builder as TB import qualified Data.Aeson.Encode.Pretty import Data.Aeson.Encode.Pretty ( defConfig - , keyOrder , confCompare , confTrailingNewline ) diff --git a/src/haskell/lib/Arion/DockerCompose.hs b/src/haskell/lib/Arion/DockerCompose.hs index b7cce7f..f44d86f 100644 --- a/src/haskell/lib/Arion/DockerCompose.hs +++ b/src/haskell/lib/Arion/DockerCompose.hs @@ -3,24 +3,7 @@ module Arion.DockerCompose where import Prelude ( ) import Protolude -import Arion.Aeson ( pretty ) -import Data.Aeson -import qualified Data.String import System.Process -import qualified Data.ByteString as BS -import qualified Data.ByteString.Lazy as BL -import Paths_arion_compose -import Control.Applicative - -import qualified Data.Text as T -import qualified Data.Text.IO as T - -import qualified Data.List.NonEmpty as NE -import Data.List.NonEmpty ( NonEmpty(..) ) - -import Control.Arrow ( (>>>) ) -import System.IO.Temp ( withTempFile ) -import System.IO ( hClose ) data Args = Args { files :: [FilePath] @@ -43,6 +26,5 @@ run args = do case exitCode of ExitSuccess -> pass ExitFailure 1 -> exitFailure - e@ExitFailure {} -> do + ExitFailure {} -> do throwIO $ FatalError $ "docker-compose failed with " <> show exitCode - exitWith e diff --git a/src/haskell/lib/Arion/Images.hs b/src/haskell/lib/Arion/Images.hs index 369d09f..a533cec 100644 --- a/src/haskell/lib/Arion/Images.hs +++ b/src/haskell/lib/Arion/Images.hs @@ -10,7 +10,6 @@ import Protolude hiding (to) import qualified Data.Aeson as Aeson import Arion.Aeson (decodeFile) -import qualified Data.ByteString as BS import qualified System.Process as Process import Control.Lens diff --git a/src/haskell/lib/Arion/Nix.hs b/src/haskell/lib/Arion/Nix.hs index a90343a..dbc9103 100644 --- a/src/haskell/lib/Arion/Nix.hs +++ b/src/haskell/lib/Arion/Nix.hs @@ -16,12 +16,9 @@ import Data.Aeson import qualified Data.String import qualified System.Directory as Directory import System.Process -import qualified Data.ByteString as BS import qualified Data.ByteString.Lazy as BL import Paths_arion_compose -import Control.Applicative -import qualified Data.Text as T import qualified Data.Text.IO as T import qualified Data.List.NonEmpty as NE @@ -76,21 +73,20 @@ evaluateComposition ea = do case exitCode of ExitSuccess -> pass ExitFailure 1 -> exitFailure - e@ExitFailure {} -> do + ExitFailure {} -> do throwIO $ FatalError $ "evaluation failed with " <> show exitCode - exitWith e case v of Right r -> pure r - Left e -> throwIO $ FatalError "Couldn't parse nix-instantiate output" + Left e -> throwIO $ FatalError ("Couldn't parse nix-instantiate output" <> show e) -- | Run with docker-compose.yaml tmpfile withEvaluatedComposition :: EvaluationArgs -> (FilePath -> IO r) -> IO r withEvaluatedComposition ea f = do v <- evaluateComposition ea - withTempFile "." ".tmp-arion-docker-compose.yaml" $ \path handle -> do - T.hPutStrLn handle (pretty v) - hClose handle + withTempFile "." ".tmp-arion-docker-compose.yaml" $ \path yamlHandle -> do + T.hPutStrLn yamlHandle (pretty v) + hClose yamlHandle f path @@ -117,15 +113,14 @@ buildComposition outLink ea = do case exitCode of ExitSuccess -> pass ExitFailure 1 -> exitFailure - e@ExitFailure {} -> do + ExitFailure {} -> do throwIO $ FatalError $ "nix-build failed with " <> show exitCode - exitWith e -- | Do something with a docker-compose.yaml. withBuiltComposition :: EvaluationArgs -> (FilePath -> IO r) -> IO r withBuiltComposition ea f = do - withTempFile "." ".tmp-arion-docker-compose.yaml" $ \path handle -> do - hClose handle + withTempFile "." ".tmp-arion-docker-compose.yaml" $ \path emptyYamlHandle -> do + hClose emptyYamlHandle -- Known problem: kills atomicity of withTempFile; won't fix because we should manage gc roots, -- impl of which will probably avoid this "problem". It seems unlikely to cause issues. Directory.removeFile path @@ -149,9 +144,8 @@ replForComposition ea = do case exitCode of ExitSuccess -> pass ExitFailure 1 -> exitFailure - e@ExitFailure {} -> do + ExitFailure {} -> do throwIO $ FatalError $ "nix repl failed with " <> show exitCode - exitWith e argArgs :: EvaluationArgs -> [[Char]] argArgs ea = diff --git a/src/haskell/lib/Arion/Services.hs b/src/haskell/lib/Arion/Services.hs index f63e6e2..d8ecb30 100644 --- a/src/haskell/lib/Arion/Services.hs +++ b/src/haskell/lib/Arion/Services.hs @@ -10,13 +10,9 @@ import Protolude hiding (to) import qualified Data.Aeson as Aeson import Arion.Aeson (decodeFile) -import qualified Data.ByteString as BS -import qualified System.Process as Process import Control.Lens import Data.Aeson.Lens -import Data.String -import System.IO (withFile, IOMode(ReadMode)) -- | Subject to change getDefaultExec :: FilePath -> Text -> IO [Text] diff --git a/src/haskell/test/Arion/NixSpec.hs b/src/haskell/test/Arion/NixSpec.hs index 884a68a..f5c3887 100644 --- a/src/haskell/test/Arion/NixSpec.hs +++ b/src/haskell/test/Arion/NixSpec.hs @@ -6,16 +6,11 @@ where import Protolude import Test.Hspec -import Test.QuickCheck import qualified Data.List.NonEmpty as NEL import Arion.Aeson import Arion.Nix import qualified Data.Text as T import qualified Data.Text.IO as T -import qualified Data.Text.Lazy.IO as TL -import qualified Data.Text.Lazy.Builder as TB -import qualified Data.Aeson.Encode.Pretty -import Data.Char (isSpace) spec :: Spec spec = describe "evaluateComposition" $ it "matches an example" $ do @@ -32,8 +27,8 @@ spec = describe "evaluateComposition" $ it "matches an example" $ do expected <- T.readFile "src/haskell/testdata/Arion/NixSpec/arion-compose.json" censorPaths actual `shouldBe` censorPaths expected +censorPaths :: Text -> Text censorPaths = censorImages . censorStorePaths ---censorPaths = censorStorePaths censorStorePaths :: Text -> Text censorStorePaths x = case T.breakOn "/nix/store/" x of @@ -61,4 +56,4 @@ isNixNameChar c | c >= 'A' && c <= 'Z' = True isNixNameChar c | c == '-' = True isNixNameChar c | c == '.' = True isNixNameChar c | c == '_' = True -- WRONG? -isNixNameChar c = False -- WRONG? +isNixNameChar _ = False -- WRONG? diff --git a/src/haskell/test/TestMain.hs b/src/haskell/test/TestMain.hs index 746b8d7..e0303fa 100644 --- a/src/haskell/test/TestMain.hs +++ b/src/haskell/test/TestMain.hs @@ -1,5 +1,6 @@ module Main where +import Prelude() import Protolude import Test.Hspec.Runner import qualified Spec