Add cabal check to ci, build strictly (once)
This commit is contained in:
parent
41d4fefd64
commit
7749eb2ef9
11 changed files with 49 additions and 73 deletions
|
@ -1,9 +1,9 @@
|
||||||
cabal-version: 2.4
|
cabal-version: 2.4
|
||||||
|
|
||||||
name: arion-compose
|
name: arion-compose
|
||||||
version: 0.1.0.0
|
version: 0.1.0.0
|
||||||
synopsis: Run docker-compose with help from Nix/NixOS
|
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
|
homepage: https://github.com/hercules-ci/arion#readme
|
||||||
-- bug-reports:
|
-- bug-reports:
|
||||||
license: Apache-2.0
|
license: Apache-2.0
|
||||||
|
@ -11,10 +11,10 @@ license-file: LICENSE
|
||||||
author: Robert Hensing
|
author: Robert Hensing
|
||||||
maintainer: robert@hercules-ci.com
|
maintainer: robert@hercules-ci.com
|
||||||
-- copyright:
|
-- copyright:
|
||||||
-- category:
|
category: Distribution, Network, Cloud, Distributed Computing
|
||||||
extra-source-files: CHANGELOG.md, README.asciidoc
|
extra-source-files: CHANGELOG.md, README.asciidoc,
|
||||||
write-ghc-enviroment-files:
|
src/haskell/testdata/**/*.nix
|
||||||
never
|
src/haskell/testdata/**/*.json
|
||||||
data-files: nix/*.nix
|
data-files: nix/*.nix
|
||||||
, nix/modules/composition/*.nix
|
, nix/modules/composition/*.nix
|
||||||
, nix/modules/nixos/*.nix
|
, nix/modules/nixos/*.nix
|
||||||
|
@ -24,7 +24,7 @@ data-files: nix/*.nix
|
||||||
-- all data is verbatim from some sources
|
-- all data is verbatim from some sources
|
||||||
data-dir: src
|
data-dir: src
|
||||||
|
|
||||||
common deps
|
common common
|
||||||
build-depends: base ^>=4.12.0.0
|
build-depends: base ^>=4.12.0.0
|
||||||
, aeson
|
, aeson
|
||||||
, aeson-pretty
|
, aeson-pretty
|
||||||
|
@ -38,25 +38,27 @@ common deps
|
||||||
, text
|
, text
|
||||||
, protolude
|
, protolude
|
||||||
, unix
|
, unix
|
||||||
|
ghc-options: -Wall
|
||||||
|
|
||||||
flag ghci
|
flag ghci
|
||||||
default: False
|
default: False
|
||||||
manual: True
|
manual: True
|
||||||
|
|
||||||
library
|
library
|
||||||
import: deps
|
import: common
|
||||||
exposed-modules: Arion.Nix
|
exposed-modules: Arion.Nix
|
||||||
Arion.Aeson
|
Arion.Aeson
|
||||||
Arion.DockerCompose
|
Arion.DockerCompose
|
||||||
Arion.Images
|
Arion.Images
|
||||||
Arion.Services
|
Arion.Services
|
||||||
other-modules: Paths_arion_compose
|
other-modules: Paths_arion_compose
|
||||||
|
autogen-modules: Paths_arion_compose
|
||||||
-- other-extensions:
|
-- other-extensions:
|
||||||
hs-source-dirs: src/haskell/lib
|
hs-source-dirs: src/haskell/lib
|
||||||
default-language: Haskell2010
|
default-language: Haskell2010
|
||||||
|
|
||||||
executable arion
|
executable arion
|
||||||
import: deps
|
import: common
|
||||||
main-is: Main.hs
|
main-is: Main.hs
|
||||||
-- other-modules:
|
-- other-modules:
|
||||||
-- other-extensions:
|
-- other-extensions:
|
||||||
|
@ -66,7 +68,7 @@ executable arion
|
||||||
default-language: Haskell2010
|
default-language: Haskell2010
|
||||||
|
|
||||||
test-suite arion-unit-tests
|
test-suite arion-unit-tests
|
||||||
import: deps
|
import: common
|
||||||
if flag(ghci)
|
if flag(ghci)
|
||||||
hs-source-dirs: src/haskell/lib
|
hs-source-dirs: src/haskell/lib
|
||||||
ghc-options: -Wno-missing-home-modules
|
ghc-options: -Wno-missing-home-modules
|
||||||
|
|
|
@ -7,6 +7,7 @@ in
|
||||||
dimension "Nixpkgs version" {
|
dimension "Nixpkgs version" {
|
||||||
"nixos-19_03" = {
|
"nixos-19_03" = {
|
||||||
nixpkgsSource = "nixpkgs";
|
nixpkgsSource = "nixpkgs";
|
||||||
|
isReferenceNixpkgs = true;
|
||||||
};
|
};
|
||||||
"nixos-unstable" = {
|
"nixos-unstable" = {
|
||||||
nixpkgsSource = "nixos-unstable";
|
nixpkgsSource = "nixos-unstable";
|
||||||
|
@ -16,15 +17,15 @@ dimension "Nixpkgs version" {
|
||||||
enableDoc = false;
|
enableDoc = false;
|
||||||
};
|
};
|
||||||
} (
|
} (
|
||||||
_name: { nixpkgsSource, enableDoc ? true }:
|
_name: { nixpkgsSource, isReferenceNixpkgs ? false, enableDoc ? true }:
|
||||||
|
|
||||||
|
|
||||||
dimension "System" {
|
dimension "System" {
|
||||||
"x86_64-linux" = {};
|
"x86_64-linux" = { isReferenceTarget = isReferenceNixpkgs; };
|
||||||
# TODO: darwin
|
# TODO: darwin
|
||||||
# "x86_64-darwin" = { enableNixOSTests = false; };
|
# "x86_64-darwin" = { enableNixOSTests = false; };
|
||||||
} (
|
} (
|
||||||
system: {}:
|
system: { isReferenceTarget ? false }:
|
||||||
let
|
let
|
||||||
pkgs = import ./. { inherit system; nixpkgsSrc = sources.${nixpkgsSource}; };
|
pkgs = import ./. { inherit system; nixpkgsSrc = sources.${nixpkgsSource}; };
|
||||||
in
|
in
|
||||||
|
@ -32,6 +33,8 @@ dimension "Nixpkgs version" {
|
||||||
inherit (pkgs) arion tests;
|
inherit (pkgs) arion tests;
|
||||||
} // lib.optionalAttrs enableDoc {
|
} // lib.optionalAttrs enableDoc {
|
||||||
doc = pkgs.recurseIntoAttrs (import ../doc { inherit pkgs; });
|
doc = pkgs.recurseIntoAttrs (import ../doc { inherit pkgs; });
|
||||||
|
} // lib.optionalAttrs isReferenceTarget {
|
||||||
|
inherit (pkgs.arion-project.haskellPkgs) arion-compose-checked;
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,4 +1,16 @@
|
||||||
self: super: hself: hsuper:
|
self: super: hself: hsuper:
|
||||||
{
|
{
|
||||||
arion-compose = import ./haskell-arion-compose.nix { pkgs = self; haskellPackages = hself; };
|
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;
|
||||||
}
|
}
|
|
@ -12,20 +12,13 @@ import qualified Arion.DockerCompose as DockerCompose
|
||||||
import Arion.Services (getDefaultExec)
|
import Arion.Services (getDefaultExec)
|
||||||
|
|
||||||
import Options.Applicative
|
import Options.Applicative
|
||||||
import Control.Applicative
|
|
||||||
import Control.Monad.Fail
|
import Control.Monad.Fail
|
||||||
|
|
||||||
import qualified Data.Aeson.Encode.Pretty
|
|
||||||
import qualified Data.Text as T
|
import qualified Data.Text as T
|
||||||
import qualified Data.Text.IO 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 Data.List.NonEmpty (NonEmpty(..))
|
||||||
|
|
||||||
import Control.Arrow ((>>>))
|
|
||||||
|
|
||||||
import System.Posix.User (getRealUserID)
|
import System.Posix.User (getRealUserID)
|
||||||
|
|
||||||
data CommonOptions =
|
data CommonOptions =
|
||||||
|
@ -69,6 +62,7 @@ parseOptions = do
|
||||||
let nixArgs = userNixArgs <|> "--show-trace" <$ guard showTrace
|
let nixArgs = userNixArgs <|> "--show-trace" <$ guard showTrace
|
||||||
in CommonOptions{..}
|
in CommonOptions{..}
|
||||||
|
|
||||||
|
textArgument :: Mod ArgumentFields [Char] -> Parser Text
|
||||||
textArgument = fmap T.pack . strArgument
|
textArgument = fmap T.pack . strArgument
|
||||||
|
|
||||||
parseCommand :: Parser (CommonOptions -> IO ())
|
parseCommand :: Parser (CommonOptions -> IO ())
|
||||||
|
@ -124,18 +118,18 @@ commandDC
|
||||||
-> Text
|
-> Text
|
||||||
-> Text
|
-> Text
|
||||||
-> Mod CommandFields (CommonOptions -> IO ())
|
-> Mod CommandFields (CommonOptions -> IO ())
|
||||||
commandDC run cmdStr help =
|
commandDC run cmdStr helpText =
|
||||||
command
|
command
|
||||||
(T.unpack cmdStr)
|
(T.unpack cmdStr)
|
||||||
(info
|
(info
|
||||||
(run cmdStr <$> parseDockerComposeArgs)
|
(run cmdStr <$> parseDockerComposeArgs)
|
||||||
(progDesc (T.unpack help) <> fullDesc <> forwardOptions))
|
(progDesc (T.unpack helpText) <> fullDesc <> forwardOptions))
|
||||||
|
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
runDC :: Text -> DockerComposeArgs -> CommonOptions -> IO ()
|
runDC :: Text -> DockerComposeArgs -> CommonOptions -> IO ()
|
||||||
runDC cmd (DockerComposeArgs args) opts = do
|
runDC cmd (DockerComposeArgs args) _opts = do
|
||||||
DockerCompose.run DockerCompose.Args
|
DockerCompose.run DockerCompose.Args
|
||||||
{ files = []
|
{ files = []
|
||||||
, otherArgs = [cmd] ++ args
|
, otherArgs = [cmd] ++ args
|
||||||
|
@ -265,7 +259,7 @@ runExec detach privileged user noTTY index envs workDir service commandAndArgs o
|
||||||
|
|
||||||
main :: IO ()
|
main :: IO ()
|
||||||
main =
|
main =
|
||||||
(join . execParser) (info (parseAll <**> helper) fullDesc)
|
(join . arionExecParser) (info (parseAll <**> helper) fullDesc)
|
||||||
where
|
where
|
||||||
execParser = customExecParser (prefs showHelpOnEmpty)
|
arionExecParser = customExecParser (prefs showHelpOnEmpty)
|
||||||
|
|
||||||
|
|
|
@ -4,11 +4,9 @@ import Prelude ()
|
||||||
import Data.Aeson
|
import Data.Aeson
|
||||||
import qualified Data.ByteString.Lazy as BL
|
import qualified Data.ByteString.Lazy as BL
|
||||||
import qualified Data.Text.Lazy as TL
|
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.Text.Lazy.Builder as TB
|
||||||
import qualified Data.Aeson.Encode.Pretty
|
import qualified Data.Aeson.Encode.Pretty
|
||||||
import Data.Aeson.Encode.Pretty ( defConfig
|
import Data.Aeson.Encode.Pretty ( defConfig
|
||||||
, keyOrder
|
|
||||||
, confCompare
|
, confCompare
|
||||||
, confTrailingNewline
|
, confTrailingNewline
|
||||||
)
|
)
|
||||||
|
|
|
@ -3,24 +3,7 @@ module Arion.DockerCompose where
|
||||||
|
|
||||||
import Prelude ( )
|
import Prelude ( )
|
||||||
import Protolude
|
import Protolude
|
||||||
import Arion.Aeson ( pretty )
|
|
||||||
import Data.Aeson
|
|
||||||
import qualified Data.String
|
|
||||||
import System.Process
|
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
|
data Args = Args
|
||||||
{ files :: [FilePath]
|
{ files :: [FilePath]
|
||||||
|
@ -43,6 +26,5 @@ run args = do
|
||||||
case exitCode of
|
case exitCode of
|
||||||
ExitSuccess -> pass
|
ExitSuccess -> pass
|
||||||
ExitFailure 1 -> exitFailure
|
ExitFailure 1 -> exitFailure
|
||||||
e@ExitFailure {} -> do
|
ExitFailure {} -> do
|
||||||
throwIO $ FatalError $ "docker-compose failed with " <> show exitCode
|
throwIO $ FatalError $ "docker-compose failed with " <> show exitCode
|
||||||
exitWith e
|
|
||||||
|
|
|
@ -10,7 +10,6 @@ import Protolude hiding (to)
|
||||||
|
|
||||||
import qualified Data.Aeson as Aeson
|
import qualified Data.Aeson as Aeson
|
||||||
import Arion.Aeson (decodeFile)
|
import Arion.Aeson (decodeFile)
|
||||||
import qualified Data.ByteString as BS
|
|
||||||
import qualified System.Process as Process
|
import qualified System.Process as Process
|
||||||
|
|
||||||
import Control.Lens
|
import Control.Lens
|
||||||
|
|
|
@ -16,12 +16,9 @@ import Data.Aeson
|
||||||
import qualified Data.String
|
import qualified Data.String
|
||||||
import qualified System.Directory as Directory
|
import qualified System.Directory as Directory
|
||||||
import System.Process
|
import System.Process
|
||||||
import qualified Data.ByteString as BS
|
|
||||||
import qualified Data.ByteString.Lazy as BL
|
import qualified Data.ByteString.Lazy as BL
|
||||||
import Paths_arion_compose
|
import Paths_arion_compose
|
||||||
import Control.Applicative
|
|
||||||
|
|
||||||
import qualified Data.Text as T
|
|
||||||
import qualified Data.Text.IO as T
|
import qualified Data.Text.IO as T
|
||||||
|
|
||||||
import qualified Data.List.NonEmpty as NE
|
import qualified Data.List.NonEmpty as NE
|
||||||
|
@ -76,21 +73,20 @@ evaluateComposition ea = do
|
||||||
case exitCode of
|
case exitCode of
|
||||||
ExitSuccess -> pass
|
ExitSuccess -> pass
|
||||||
ExitFailure 1 -> exitFailure
|
ExitFailure 1 -> exitFailure
|
||||||
e@ExitFailure {} -> do
|
ExitFailure {} -> do
|
||||||
throwIO $ FatalError $ "evaluation failed with " <> show exitCode
|
throwIO $ FatalError $ "evaluation failed with " <> show exitCode
|
||||||
exitWith e
|
|
||||||
|
|
||||||
case v of
|
case v of
|
||||||
Right r -> pure r
|
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
|
-- | Run with docker-compose.yaml tmpfile
|
||||||
withEvaluatedComposition :: EvaluationArgs -> (FilePath -> IO r) -> IO r
|
withEvaluatedComposition :: EvaluationArgs -> (FilePath -> IO r) -> IO r
|
||||||
withEvaluatedComposition ea f = do
|
withEvaluatedComposition ea f = do
|
||||||
v <- evaluateComposition ea
|
v <- evaluateComposition ea
|
||||||
withTempFile "." ".tmp-arion-docker-compose.yaml" $ \path handle -> do
|
withTempFile "." ".tmp-arion-docker-compose.yaml" $ \path yamlHandle -> do
|
||||||
T.hPutStrLn handle (pretty v)
|
T.hPutStrLn yamlHandle (pretty v)
|
||||||
hClose handle
|
hClose yamlHandle
|
||||||
f path
|
f path
|
||||||
|
|
||||||
|
|
||||||
|
@ -117,15 +113,14 @@ buildComposition outLink ea = do
|
||||||
case exitCode of
|
case exitCode of
|
||||||
ExitSuccess -> pass
|
ExitSuccess -> pass
|
||||||
ExitFailure 1 -> exitFailure
|
ExitFailure 1 -> exitFailure
|
||||||
e@ExitFailure {} -> do
|
ExitFailure {} -> do
|
||||||
throwIO $ FatalError $ "nix-build failed with " <> show exitCode
|
throwIO $ FatalError $ "nix-build failed with " <> show exitCode
|
||||||
exitWith e
|
|
||||||
|
|
||||||
-- | Do something with a docker-compose.yaml.
|
-- | Do something with a docker-compose.yaml.
|
||||||
withBuiltComposition :: EvaluationArgs -> (FilePath -> IO r) -> IO r
|
withBuiltComposition :: EvaluationArgs -> (FilePath -> IO r) -> IO r
|
||||||
withBuiltComposition ea f = do
|
withBuiltComposition ea f = do
|
||||||
withTempFile "." ".tmp-arion-docker-compose.yaml" $ \path handle -> do
|
withTempFile "." ".tmp-arion-docker-compose.yaml" $ \path emptyYamlHandle -> do
|
||||||
hClose handle
|
hClose emptyYamlHandle
|
||||||
-- Known problem: kills atomicity of withTempFile; won't fix because we should manage gc roots,
|
-- 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.
|
-- impl of which will probably avoid this "problem". It seems unlikely to cause issues.
|
||||||
Directory.removeFile path
|
Directory.removeFile path
|
||||||
|
@ -149,9 +144,8 @@ replForComposition ea = do
|
||||||
case exitCode of
|
case exitCode of
|
||||||
ExitSuccess -> pass
|
ExitSuccess -> pass
|
||||||
ExitFailure 1 -> exitFailure
|
ExitFailure 1 -> exitFailure
|
||||||
e@ExitFailure {} -> do
|
ExitFailure {} -> do
|
||||||
throwIO $ FatalError $ "nix repl failed with " <> show exitCode
|
throwIO $ FatalError $ "nix repl failed with " <> show exitCode
|
||||||
exitWith e
|
|
||||||
|
|
||||||
argArgs :: EvaluationArgs -> [[Char]]
|
argArgs :: EvaluationArgs -> [[Char]]
|
||||||
argArgs ea =
|
argArgs ea =
|
||||||
|
|
|
@ -10,13 +10,9 @@ import Protolude hiding (to)
|
||||||
|
|
||||||
import qualified Data.Aeson as Aeson
|
import qualified Data.Aeson as Aeson
|
||||||
import Arion.Aeson (decodeFile)
|
import Arion.Aeson (decodeFile)
|
||||||
import qualified Data.ByteString as BS
|
|
||||||
import qualified System.Process as Process
|
|
||||||
|
|
||||||
import Control.Lens
|
import Control.Lens
|
||||||
import Data.Aeson.Lens
|
import Data.Aeson.Lens
|
||||||
import Data.String
|
|
||||||
import System.IO (withFile, IOMode(ReadMode))
|
|
||||||
|
|
||||||
-- | Subject to change
|
-- | Subject to change
|
||||||
getDefaultExec :: FilePath -> Text -> IO [Text]
|
getDefaultExec :: FilePath -> Text -> IO [Text]
|
||||||
|
|
|
@ -6,16 +6,11 @@ where
|
||||||
|
|
||||||
import Protolude
|
import Protolude
|
||||||
import Test.Hspec
|
import Test.Hspec
|
||||||
import Test.QuickCheck
|
|
||||||
import qualified Data.List.NonEmpty as NEL
|
import qualified Data.List.NonEmpty as NEL
|
||||||
import Arion.Aeson
|
import Arion.Aeson
|
||||||
import Arion.Nix
|
import Arion.Nix
|
||||||
import qualified Data.Text as T
|
import qualified Data.Text as T
|
||||||
import qualified Data.Text.IO 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 :: Spec
|
||||||
spec = describe "evaluateComposition" $ it "matches an example" $ do
|
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"
|
expected <- T.readFile "src/haskell/testdata/Arion/NixSpec/arion-compose.json"
|
||||||
censorPaths actual `shouldBe` censorPaths expected
|
censorPaths actual `shouldBe` censorPaths expected
|
||||||
|
|
||||||
|
censorPaths :: Text -> Text
|
||||||
censorPaths = censorImages . censorStorePaths
|
censorPaths = censorImages . censorStorePaths
|
||||||
--censorPaths = censorStorePaths
|
|
||||||
|
|
||||||
censorStorePaths :: Text -> Text
|
censorStorePaths :: Text -> Text
|
||||||
censorStorePaths x = case T.breakOn "/nix/store/" x of
|
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
|
isNixNameChar c | c == '.' = True
|
||||||
isNixNameChar c | c == '_' = True -- WRONG?
|
isNixNameChar c | c == '_' = True -- WRONG?
|
||||||
isNixNameChar c = False -- WRONG?
|
isNixNameChar _ = False -- WRONG?
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
module Main where
|
module Main where
|
||||||
|
|
||||||
|
import Prelude()
|
||||||
import Protolude
|
import Protolude
|
||||||
import Test.Hspec.Runner
|
import Test.Hspec.Runner
|
||||||
import qualified Spec
|
import qualified Spec
|
||||||
|
|
Loading…
Reference in a new issue