Commit f6f51d37 authored by Michael Hanus 's avatar Michael Hanus

Tool installation and test suite specification added

parent d024dce0
......@@ -45,8 +45,9 @@
\title{CPM User's Manual}
\author{Jonas Oberschweiber\footnote{and some extensions by Michael Hanus}\\[1ex]
{\small Institut f\"ur Informatik, CAU Kiel, Germany}
\author{Jonas Oberschweiber \qquad Michael Hanus\\[1ex]
{\small Institut f\"ur Informatik, CAU Kiel, Germany}\\[1ex]
{\small\texttt{packages@curry-language.org}}
}
\maketitle
......@@ -137,11 +138,18 @@ Conjunctions can be combined into a disjunction via the $||$ characters, e.g.
$\geq 2.0.0, < 3.0.0 || \geq 4.0.0$ would match any version within major version
$2$ and from major version $4$ onwards, but no version within major version $3$.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
\section{Using Packages}
Curry packages can only be used as dependencies of other Curry packages.
Luckily creating a Curry package is easy, as we have seen in the previous
section. So, to use a Curry package in your project, create a
Curry packages can be used as dependencies of other Curry packages
or to install applications implemented with a package.
In the following we describe both possibilities of using packages.
\subsection{Creating New Packages}
Creating a new Curry package is easy.
To use a Curry package in your project, create a
\code{package.json} file in the root, fill it with the minimum amount of
information discussed in the previous session, and move your Curry code to a
\code{src} directory inside your project's directory. Alternatively, if you are
......@@ -161,7 +169,8 @@ will ask you a few questions and then create a new project directory with a
%
Then run \code{cpm install} to install all dependencies of the current package
and start your interactive Curry environment with \code{cpm curry}. You will be
able to load the JSON package's modules.
able to load the JSON package's modules in your Curry session.
\subsection{Installing and Updating Dependencies}
......@@ -184,6 +193,43 @@ search the central package index via the \code{cpm search}
command. See Section~\ref{sec:cmd-reference} for a reference of all
commands.
If the package also contains an implementation of a complete executable,
e.g., some useful tool,
which can be specifed in the \code{package.json} file
(see Section~\ref{sec:reference}),
then the command \code{cpm install} also compiles the application
and installs the executable in the \code{bin} install directory of CPM
(see Section~\ref{sec:config} for details).
\subsection{Checking out Packages}
In order to use, experiment with or modify an existing package,
one can use the command
\begin{lstlisting}
cpm checkout <package>
\end{lstlisting}
to install a local copy of a package.
This is also useful to install some tool distributed as a package.
For instance, to install \code{curry-genmake},
a tool to generate a \code{make} file for a Curry application,
one can check out the most recent version and install the tool:
%
\begin{lstlisting}
> cpm checkout makefile
$\ldots$ Package 'makefile-0.0.1' checked out $\ldots$
> cd makefile-0.0.1
> cpm install
$\ldots$
INFO Installing executable `curry-genmake' into `/home/joe/.cpm/bin'
\end{lstlisting}
%
Now, the tool \code{curry-genmake} is ready to use
if \code{\$HOME/.cpm/bin} is in your path
(see Section~\ref{sec:config} for details about changing the location
of this default path).
\subsection{Executing the Compiler}
To use the dependencies of a package, the Curry compiler needs to be started via
......@@ -195,7 +241,7 @@ start the Curry compiler, print the result of evaluating the expression
\code{39+3} and then quit.
\begin{lstlisting}
cpm curry :eval "39+3" :quit
> cpm curry :eval "39+3" :quit
\end{lstlisting}
%
To execute other Curry commands such as \code{curry check} with the package's
......@@ -401,6 +447,7 @@ Otherwise, send your package specification file to
\section{Configuration}
\label{sec:config}
CPM can be configured via the \code{\$HOME/.cpmrc} configuration file. The
following list shows all configuration options and their default values.
......@@ -410,6 +457,13 @@ following list shows all configuration options and their default values.
This is where all downloaded packages are stored. Default value:
\code{\$HOME/.cpm/packages}
\item[\fbox{\code{bin_install_path}}] The path to the executables
of packages. This is the location where the compiled executables
of packages containing full applications are stored.
Hence, in order to use such applications, one should have this path
in the personal load path (environment variable \code{PATH}).
Default value: \code{\$HOME/.cpm/bin}
\item[\fbox{\code{repository_path}}] The path to the index repository. Default
value: \code{\$HOME/.cpm/index}.
\end{description}
......@@ -512,14 +566,19 @@ package and prints out the results. The result is either a list of all package
versions chosen or a description of the conflict encountered during dependency
resolution.
\item[\fbox{\code{check}}]
Checks the current package with CurryCheck.
If the package specifies a list of exported modules
\item[\fbox{\code{test}}]
Tests the current package with CurryCheck.
If the package specification contains a definition of a test suite
(entry \code{testsuite}, see Section~\ref{sec:reference}),
then the modules defined there are tested.
If there is no test suite defined,
the list of exported modules are tested,
if they are explicitly specified
(field \code{exportedModules} of the package specification),
these modules are checked, otherwise all modules in the directory \code{src}
(including hierarchical modules stored in its subdirectories) are checked.
otherwise all modules in the directory \code{src}
(including hierarchical modules stored in its subdirectories) are tested.
Using the option \code{--modules}, one can also specify a comma-separated
list of module names to be checked.
list of module names to be tested.
\item[\fbox{\code{diff [$version$]}}]
Compares the API and behavior of the current package to another
......@@ -626,10 +685,62 @@ package described in the specification can be obtained. See
Section~\ref{sec:publishing-a-package} for details.
\item[\fbox{\code{exportedModules}}] A list of modules intended for use by
consumers of the package. These are the modules compared by the \code{cpm diff}
command or checked by the \code{cpm check} command.
consumers of the package.
These are the modules compared by the \code{cpm diff}
command (and tested by the \code{cpm test} command if a list of
test modules is not provided).
Note that modules not in this list are still accessible to consumers
of the package.
\item[\fbox{\code{executable}}]
A JSON object specifying the name of the executable and the main module
if this package contains also an executable application.
The name of the executable must be defined (with key \code{name})
whereas the name of the main module (key \code{main}) is optional.
If the latter is missing, CPM assumes that the main module is \code{Main}.
For instance, a possible specification could be as follows:
%
\begin{lstlisting}
{
...,
"executable": {
"name": "cpm",
"main": "CPM.Main"
}
}
\end{lstlisting}
%
If a package contains an \code{executable} specification,
the command \code{cpm install} compiles the main module
and installs the executable in the \code{bin} install directory of CPM
(see Section~\ref{sec:config} for details).
\item[\fbox{\code{testsuite}}]
A JSON object specifying a test suite for this package.
This object can contain a list of directories (with key \code{src-dirs})
which are added in front of the \code{CURRYPATH} environment variable
before executing the tests.
This might be useful if there are test modules in a directory
different from the default directory \code{src}.
Furthermore, the test suite must also define a list of
modules to be tested (with key \code{modules}).
For instance, a possible test suite specification could be as follows:
%
\begin{lstlisting}
{
...,
"testsuite": {
"src-dirs": [ "test" ],
"modules" : [ "testDataConversion", "testIO" ]
}
}
\end{lstlisting}
%
Note that all these modules are tested with CurryCheck
by the command \code{cpm test}.
If no test suite is defined, all (exported) modules in the source
directory are tested.
\end{description}
\end{document}
......@@ -5,7 +5,8 @@
--------------------------------------------------------------------------------
module CPM.Config
( Config (Config, packageInstallDir, repositoryDir, packageIndexRepository)
( Config ( Config, packageInstallDir, binInstallDir, repositoryDir
, packageIndexRepository )
, readConfiguration, defaultConfig) where
import Char (isSpace)
......@@ -28,6 +29,8 @@ packageIndexURI = "https://git.ps.informatik.uni-kiel.de/curry/cpm-index.git"
data Config = Config {
--- The directory where locally installed packages are stored
packageInstallDir :: String
--- The directory where executable of locally installed packages are stored
, binInstallDir :: String
--- Directory where the package repository is stored
, repositoryDir :: String
--- URL to the package index repository
......@@ -39,6 +42,7 @@ data Config = Config {
defaultConfig :: Config
defaultConfig = Config
{ packageInstallDir = "$HOME/.cpm/packages"
, binInstallDir = "$HOME/.cpm/bin"
, repositoryDir = "$HOME/.cpm/index"
, packageIndexRepository = packageIndexURI }
......@@ -56,7 +60,7 @@ readConfiguration = do
else return []
mergedSettings <- return $ mergeConfigFile defaultConfig settingsFromFile
case mergedSettings of
Left e -> return $ Left e
Left e -> return $ Left e
Right s' -> replaceHome s' >>= \s'' -> createDirectories s'' >>
return (Right s'')
......@@ -65,7 +69,8 @@ replaceHome cfg = do
homeDir <- getHomeDirectory
return $ cfg {
packageInstallDir = replaceHome' homeDir (packageInstallDir cfg)
, repositoryDir = replaceHome' homeDir (repositoryDir cfg)
, binInstallDir = replaceHome' homeDir (binInstallDir cfg)
, repositoryDir = replaceHome' homeDir (repositoryDir cfg)
}
where
replaceHome' h s = concat $ intersperse h $ splitOn "$HOME" s
......@@ -73,6 +78,7 @@ replaceHome cfg = do
createDirectories :: Config -> IO ()
createDirectories cfg = do
createDirectoryIfMissing True (packageInstallDir cfg)
createDirectoryIfMissing True (binInstallDir cfg)
createDirectoryIfMissing True (repositoryDir cfg)
--- Merges configuration options from a configuration file into a configuration
......@@ -99,9 +105,11 @@ stripProps = map (strip *** strip)
--- A map from option names to functions that will update a configuration
--- record with a value for that option.
keySetters :: [(String, String -> Config -> Either String Config)]
keySetters = [
("repository_path", \v c -> Right $ c { repositoryDir = v })
, ("package_install_path", \v c -> Right $ c { packageInstallDir = v}) ]
keySetters =
[ ("repository_path" , \v c -> Right $ c { repositoryDir = v })
, ("package_install_path", \v c -> Right $ c { packageInstallDir = v})
, ("bin_install_path" , \v c -> Right $ c { binInstallDir = v})
]
--- Sequentially applies a list of functions that transform a value to a value
--- of that type (i.e. a fold). Each function can error out with a Left, in
......
......@@ -214,10 +214,12 @@ callCurryCheck info baseTmp = do
setEnviron "CURRYPATH" currypath
log Debug ("Run `curry check Compare' in `" ++ baseTmp ++ "' with") |>
log Debug ("CURRYPATH=" ++ currypath) |> succeedIO ()
inDirectory baseTmp $ system (currybin ++ " check Compare")
ecode <- inDirectory baseTmp $ system (currybin ++ " check Compare")
setEnviron "CURRYPATH" oldPath
log Debug "CurryCheck finished" |> succeedIO ()
succeedIO ()
if ecode==0
then succeedIO ()
else log Error "CurryCheck detected behavior error!"
--- Generates a program containing CurryCheck tests that will compare the
--- behavior of the given functions. The program will be written to the
......
......@@ -8,7 +8,7 @@ module CPM.ErrorLogger
, LogLevel (..)
, logLevelOf
, levelGte
, setLogLevel
, getLogLevel, setLogLevel
, (|>=)
, (|>)
, (|->)
......@@ -50,6 +50,10 @@ data LogLevel = Info
logLevel :: Global LogLevel
logLevel = global Info Temporary
--- Gets the global log level. Messages below this level will not be printed.
getLogLevel :: IO LogLevel
getLogLevel = readGlobal logLevel
--- Sets the global log level. Messages below this level will not be printed.
setLogLevel :: LogLevel -> IO ()
setLogLevel level = writeGlobal logLevel level
......@@ -114,7 +118,7 @@ foldEL f z (x:xs) = do
--- Renders a log entry.
showLogEntry :: LogEntry -> IO ()
showLogEntry (LogEntry lvl msg) = do
minLevel <- readGlobal logLevel
minLevel <- getLogLevel
if levelGte lvl minLevel
then putStrLn $ pPrint $ lvlText <+> (text msg)
else return ()
......
This diff is collapsed.
......@@ -27,6 +27,7 @@ module CPM.Package
, PackageId (..)
, PackageSource (..)
, GitRevision (..)
, PackageExecutable (..), PackageTests (..)
, showDependency
, showCompilerDependency
, loadPackageSpec
......@@ -88,6 +89,16 @@ data CompilerCompatibility = CompilerCompatibility String Disjunction
--- A package id consisting of the package name and version.
data PackageId = PackageId String Version
--- The specification to generate an executable from the package.
--- It consists of the name of the executable and the name of the main
--- module (which must contain an operation `main`).
data PackageExecutable = PackageExecutable String String
--- The specification of a test suite for a package.
--- It consists of a list of directories to be included in the
--- load path and a list of modules to be tested.
data PackageTests = PackageTests [String] [String]
--- A source where the contents of a package can be acquired.
--- @cons Http - URL to a ZIP file
--- @cons Git - URL to a Git repository and an optional revision spec to check
......@@ -124,6 +135,8 @@ data Package = Package {
, compilerCompatibility :: [CompilerCompatibility]
, source :: Maybe PackageSource
, exportedModules :: [String]
, executableSpec :: Maybe PackageExecutable
, testSuite :: Maybe PackageTests
}
packageSpecToJSON :: Package -> JValue
......@@ -139,7 +152,7 @@ packageSpecToJSON pkg = JObject [
dependencyToJSON (Dependency p vc) = (p, JString $ showVersionConstraints vc)
exportedModulesToJSON exps = JArray $ map JString exps
--- Writes a package specification to a JSON file.
--- Writes a basic package specification to a JSON file.
---
--- @param pkg the package specification to write
--- @param file the file name to write to
......@@ -298,6 +311,8 @@ packageSpecFromJObject kv = mandatoryString "name" $ \name ->
getSource $ \source ->
getExportedModules $ \exportedModules ->
getCompilerCompatibility $ \compilerCompatibility ->
getExecutableSpec $ \executable ->
getTestSuite $ \testsuite ->
Right Package {
name = name
, version = version
......@@ -315,6 +330,8 @@ packageSpecFromJObject kv = mandatoryString "name" $ \name ->
, compilerCompatibility = compilerCompatibility
, source = source
, exportedModules = exportedModules
, executableSpec = executable
, testSuite = testsuite
}
where
mandatoryString :: String -> (String -> Either String a) -> Either String a
......@@ -353,6 +370,7 @@ packageSpecFromJObject kv = mandatoryString "name" $ \name ->
Just JTrue -> Left $ "Expected an object, got 'true' for key 'dependencies'"
Just JFalse -> Left $ "Expected an object, got 'false' for key 'dependencies'"
Just JNull -> Left $ "Expected an object, got 'null' for key 'dependencies'"
getCompilerCompatibility :: ([CompilerCompatibility] -> Either String a) -> Either String a
getCompilerCompatibility f = case lookup "compilerCompatibility" kv of
Nothing -> f []
......@@ -365,6 +383,7 @@ packageSpecFromJObject kv = mandatoryString "name" $ \name ->
Just JTrue -> Left $ "Expected an object, got 'true' for key 'compilerCompatibility'"
Just JFalse -> Left $ "Expected an object, got 'false' for key 'compilerCompatibility'"
Just JNull -> Left $ "Expected an object, got 'null' for key 'compilerCompatibility'"
getSource :: (Maybe PackageSource -> Either String a) -> Either String a
getSource f = case lookup "source" kv of
Nothing -> f Nothing
......@@ -377,10 +396,11 @@ packageSpecFromJObject kv = mandatoryString "name" $ \name ->
Just JTrue -> Left $ "Expected an object, got 'true' for key 'source'"
Just JFalse -> Left $ "Expected an object, got 'false' for key 'source'"
Just JNull -> Left $ "Expected an object, got 'null' for key 'source'"
getExportedModules :: ([String] -> Either String a) -> Either String a
getExportedModules f = case lookup "exportedModules" kv of
Nothing -> f []
Just (JArray a) -> case modulesFromJArray a of
Just (JArray a) -> case stringsFromJArray "An exported module" a of
Left e -> Left e
Right e -> f e
Just (JObject _) -> Left $ "Expected an array, got an object for key 'exportedModules'"
......@@ -390,6 +410,30 @@ packageSpecFromJObject kv = mandatoryString "name" $ \name ->
Just JFalse -> Left $ "Expected an array, got 'false' for key 'exportedModules'"
Just JNull -> Left $ "Expected an array, got 'null' for key 'exportedModules'"
getExecutableSpec :: (Maybe PackageExecutable -> Either String a) -> Either String a
getExecutableSpec f = case lookup "executable" kv of
Nothing -> f Nothing
Just (JObject s) -> case execSpecFromJObject s of Left e -> Left e
Right s' -> f (Just s')
Just (JString _) -> Left $ "Expected an object, got a string for key 'executable'"
Just (JArray _) -> Left $ "Expected an object, got an array for key 'executable'"
Just (JNumber _) -> Left $ "Expected an object, got a number for key 'executable'"
Just JTrue -> Left $ "Expected an object, got 'true' for key 'executable'"
Just JFalse -> Left $ "Expected an object, got 'false' for key 'executable'"
Just JNull -> Left $ "Expected an object, got 'null' for key 'executable'"
getTestSuite :: (Maybe PackageTests -> Either String a) -> Either String a
getTestSuite f = case lookup "testsuite" kv of
Nothing -> f Nothing
Just (JObject s) -> case testSuiteFromJObject s of Left e -> Left e
Right s' -> f (Just s')
Just (JString _) -> Left $ "Expected an object, got a string for key 'testsuite'"
Just (JArray _) -> Left $ "Expected an object, got an array for key 'testsuite'"
Just (JNumber _) -> Left $ "Expected an object, got a number for key 'testsuite'"
Just JTrue -> Left $ "Expected an object, got 'true' for key 'testsuite'"
Just JFalse -> Left $ "Expected an object, got 'false' for key 'testsuite'"
Just JNull -> Left $ "Expected an object, got 'null' for key 'testsuite'"
test_specFromJObject_mandatoryFields :: Test.EasyCheck.Prop
test_specFromJObject_mandatoryFields = is (packageSpecFromJObject obj) (\x -> isLeft x && isInfixOf "name" ((head . lefts) [x]))
where obj = [("hello", JString "world")]
......@@ -405,15 +449,15 @@ test_specFromJObject_minimalSpec = is (packageSpecFromJObject obj) (\x -> isRigh
where p = (head . rights) [x]
--- Reads the list of exported modules from a list of JValues.
modulesFromJArray :: [JValue] -> Either String [String]
modulesFromJArray a = if any isLeft strings
stringsFromJArray :: String -> [JValue] -> Either String [String]
stringsFromJArray ekind a = if any isLeft strings
then Left $ head $ lefts strings
else Right $ rights strings
where
strings = map extractString a
extractString s = case s of
JString s' -> Right s'
_ -> Left "An exported module must be a string"
_ -> Left $ ekind ++ " must be a string"
--- Reads the dependency constraints of a package from the key-value-pairs of a
--- JObject.
......@@ -475,6 +519,41 @@ revisionFromJObject kv = case lookup "tag" kv of
else Right $ Just $ Tag tag
Just _ -> Left "Tag expects string"
--- Read executable specification from the key-value-pairs of a JObject.
execSpecFromJObject :: [(String, JValue)] -> Either String PackageExecutable
execSpecFromJObject kv = case lookup "name" kv of
Nothing -> Left $ "Name of executable not provided"
Just (JString name) -> case lookup "main" kv of
Nothing -> Right $ PackageExecutable name "Main"
Just (JString main) -> Right $ PackageExecutable name main
Just _ -> Left $ "Main module of executable must be a string"
Just _ -> Left "Name of executable must be a string"
--- Read a test suite specification from the key-value-pairs of a JObject.
testSuiteFromJObject :: [(String, JValue)] -> Either String PackageTests
testSuiteFromJObject kv = case getOptStringList True "src-dir" kv of
Left e -> Left e
Right dirs -> case getOptStringList False "module" kv of
Left e -> Left e
Right mods -> Right (PackageTests dirs mods)
--- Reads an (optional) key with a string list value.
getOptStringList :: Bool -> String -> [(String, JValue)]
-> Either String [String]
getOptStringList optional key kv = case lookup (key++"s") kv of
Nothing -> if optional
then Right []
else Left $ "'"++key++"s' is not provided in 'testsuite'"
Just (JArray a) -> stringsFromJArray ("A "++key) a
Just (JObject _) -> Left $ "Expected an array, got an object" ++ forKey
Just (JString _) -> Left $ "Expected an array, got a string" ++ forKey
Just (JNumber _) -> Left $ "Expected an array, got a number" ++ forKey
Just JTrue -> Left $ "Expected an array, got 'true'" ++ forKey
Just JFalse -> Left $ "Expected an array, got 'false'" ++ forKey
Just JNull -> Left $ "Expected an array, got 'null'" ++ forKey
where
forKey = " for key '" ++ key ++ "s'"
--- Reads a dependency constraint expression in disjunctive normal form into
--- a list of lists of version constraints. The inner lists are conjunctions of
--- version constraints, the outer list is a disjunction of conjunctions.
......
......@@ -40,7 +40,9 @@ import CPM.Package ( Package (..)
, readPackageSpec, packageId, readVersion, Version
, showVersion, PackageSource (..), showDependency
, showCompilerDependency
, Dependency, GitRevision (..), packageIdEq, loadPackageSpec)
, Dependency, GitRevision (..), PackageExecutable (..)
, PackageTests (..)
, packageIdEq, loadPackageSpec)
import CPM.Resolution
import CPM.FileUtil (copyDirectory, recreateDirectory)
......@@ -119,7 +121,7 @@ upgradeAllPackages :: Config -> Repository -> GC.GlobalCache -> String
upgradeAllPackages cfg repo gc dir = loadPackageSpec dir |>=
\pkgSpec -> LocalCache.clearCache dir >> succeedIO () |>
installLocalDependencies cfg repo gc dir |>=
\deps -> copyDependencies cfg gc pkgSpec deps dir
\ (_,deps) -> copyDependencies cfg gc pkgSpec deps dir
--- Upgrades a single dependencies and its transitive dependencies.
upgradeSinglePackage :: Config -> Repository -> GC.GlobalCache -> String
......@@ -134,13 +136,13 @@ upgradeSinglePackage cfg repo gc dir pkgName = loadPackageSpec dir |>=
--- Installs the dependencies of a package.
installLocalDependencies :: Config -> Repository -> GC.GlobalCache -> String
-> IO (ErrorLogger [Package])
-> IO (ErrorLogger (Package,[Package]))
installLocalDependencies cfg repo gc dir = loadPackageSpec dir |>=
\pkgSpec -> resolveDependenciesForPackageCopy cfg pkgSpec repo gc dir |>=
\result -> GC.installMissingDependencies cfg gc (resolvedPackages result) |>
log Info (showDependencies result) |>
copyDependencies cfg gc pkgSpec (resolvedPackages result) dir |>
succeedIO (resolvedPackages result)
succeedIO (pkgSpec, resolvedPackages result)
--- Links a directory into the local package cache. Used for cpm link.
linkToLocalCache :: String -> String -> IO (ErrorLogger ())
......@@ -199,7 +201,8 @@ renderPackageInfo allinfos _ gc pkg = pPrint doc
doc = vcat $ [ heading, rule, installed, ver, auth, maintnr, synop
, deps, compilers, descr ] ++
if allinfos
then [expmods, src, licns, copyrt, homepg, reposy, bugrep]
then [ expmods, execspec, testsuite, src, licns, copyrt
, homepg, reposy, bugrep]
else []
pkgId = packageId pkg
......@@ -219,6 +222,24 @@ renderPackageInfo allinfos _ gc pkg = pPrint doc
Nothing -> empty
Just s -> fill maxLen (bold (text "Maintainer")) <+> text s
execspec = case executableSpec pkg of
Nothing -> empty
Just (PackageExecutable n m) ->
fill maxLen (bold (text "Executable")) <+> text n <$$>
fill maxLen (bold (text "Main module")) <+> text m
testsuite = case testSuite pkg of
Nothing -> empty
Just (PackageTests dirs mods) ->
(if null dirs
then empty
else fill maxLen (bold (text "Test dirs")) <$$>
indent 4 (fillSep (map text dirs))) <$$>
(if null mods
then empty
else fill maxLen (bold (text "Test modules"))<$$>
indent 4 (fillSep (map text mods)))
descr = showParaField description "Description"
licns = showParaField license "License"
copyrt = showParaField copyright "Copyright"
......
......@@ -258,6 +258,8 @@ samplePackageA = Package {
, compilerCompatibility = []
, source = Nothing
, exportedModules = ["Sample"]
, executableSpec = Nothing
, testSuite = Nothing
}
samplePackageB :: Package
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment