Commit f2d65b24 authored by Michael Hanus 's avatar Michael Hanus
Browse files

CPM updated

parent 2d7e006d
......@@ -45,6 +45,8 @@ src/CPM/ConfigPackage.curry: Makefile
@echo "module CPM.ConfigPackage where" > $@
@echo "packagePath :: String" >> $@
@echo "packagePath = \"$(CURDIR)\"" >> $@
@echo "packageVersion :: String" >> $@
@echo "packageVersion = \"2.0.0\"" >> $@
@echo "Curry configuration module '$@' written."
runtest:
......
......@@ -743,7 +743,17 @@ Using the option \code{--modules}, one can also specify a comma-separated
list of module names to be tested.
\item[\fbox{\code{doc}}]
Generates the HTML documentation of the current package with CurryDoc.
Generates the documentation of the current package.
The documentation consists of the API documentation (in HTML format)
and the manual (if provided) in PDF format.
The options \code{--programs} and \code{--text} forces to generate
only the API documentation and the manual, respectively.
Using the option \code{--docdir}, one can specify the
target directory where the documentation should be stored.
If this option is not provided, \ccode{cdoc} is used as the documentation
directory.
The API documentation in HTML format is generated with CurryDoc.
If the package specification contains a list of exported modules
(see Section~\ref{sec:reference}),
then these modules are documented.
......@@ -754,10 +764,10 @@ or all modules in the directory \code{src}
are documented.
Using the option \code{--modules}, one can also specify a comma-separated
list of module names to be documented.
Using the option \code{--docdir}, one can specify the
target directory where the documentation should be stored.
If this option is not provided, \ccode{cdoc} is used as the documentation
directory.
The manual is generated only if the package specification contains
a field \code{documentation} where the main file of the manual
is specified (see Section~\ref{sec:reference} for more details).
\item[\fbox{\code{diff [$version$]}}]
Compares the API and behavior of the current package to another
......@@ -1022,10 +1032,53 @@ directories \code{test} and \code{examples} could be as follows:
}
\end{lstlisting}
\item[\fbox{\code{documentation}}]
A JSON object specifying the name of the directory
which contains the sources of the documentation (e.g., a manual)
of the package, the main file of the documentation, and an optional command
to generate the documentation.
For instance, a possible specification could be as follows:
%
\begin{lstlisting}
{
...,
"documentation": {
"src-dir": "docs",
"main" : "manual.tex",
"command": "pfdlatex -output-directory=OUTDIR manual.tex"
}
...
}
\end{lstlisting}
%
In this case, the directory \code{docs} contains the sources of
the manual and \code{manual.tex} is its main file which will be
processed with the specified command.
Occurrences of the string \code{OUTDIR} in the command string
will be replaced by the actual documentation directory
(see description of the command \code{cpm doc}).
If the command is omitted, the following commands are used
(and you have to ensure that these programs are installed):
\begin{itemize}
\item
If the main file has the extension \code{.tex},
e.g., \code{manual.tex}, the command is
\begin{lstlisting}
pdflatex -output-directory=OUTDIR manual.tex
\end{lstlisting}
and it will be executed twice.
\item
If the main file has the extension \code{.md},
e.g., \code{manual.md}, the command is
\begin{lstlisting}
pandoc manual.md -o OUTDIR/manual.pdf
\end{lstlisting}
\end{itemize}
\end{description}
%
In order to provide a compact overview over all metadata fields,
we provide an example of a package specification where all fields
In order to get a compact overview over all metadata fields,
we show an example of a package specification where all fields
are used:
%
\begin{lstlisting}
......@@ -1073,6 +1126,11 @@ are used:
"script" : "test.sh"
}
],
"documentation": {
"src-dir": "docs",
"main" : "manual.tex",
"command": "pfdlatex -output-directory=OUTDIR manual.tex"
},
"source": {
"git": "URL OF THE GIT REPOSITORY",
"tag": "$\$$version"
......@@ -1095,7 +1153,7 @@ Here are some suggestions how to do this:
\begin{description}
\item[\code{cpm clean}]~\\
This command cleans the current package from
generated auxiliariy files (see Section~\ref{sec:cmd-reference}).
generated auxiliary files (see Section~\ref{sec:cmd-reference}).
Then you can re-install the package and packages on which it depends
by the command \code{cpm install}.
\item[\code{rm -rf \$HOME/.cpm/packages}] ~\\
......
......@@ -12,18 +12,19 @@ import Directory ( doesFileExist, getAbsolutePath, doesDirectoryExist
, renameFile, removeFile, setCurrentDirectory )
import Distribution ( stripCurrySuffix, addCurrySubdir )
import Either
import FilePath ( (</>), splitSearchPath, takeExtension )
import FilePath ( (</>), splitSearchPath, replaceExtension, takeExtension )
import IO ( hFlush, stdout )
import List ( groupBy, intercalate, nub, split, splitOn )
import List ( groupBy, intercalate, isSuffixOf, nub, split, splitOn )
import Sort ( sortBy )
import System ( getArgs, getEnviron, setEnviron, unsetEnviron, exitWith )
import System ( getArgs, getEnviron, setEnviron, unsetEnviron, exitWith
, system )
import Boxes (table, render)
import OptParse
import CPM.ErrorLogger
import CPM.FileUtil ( fileInPath, joinSearchPath, safeReadFile, whenFileExists
, ifFileExists, inDirectory, removeDirectoryComplete
, copyDirectory )
, copyDirectory, quote )
import CPM.Config ( Config (..)
, readConfigurationWithDefault, showCompilerVersion )
import CPM.PackageCache.Global ( GlobalCache, readInstalledPackagesFromDir
......@@ -45,7 +46,7 @@ cpmBanner :: String
cpmBanner = unlines [bannerLine,bannerText,bannerLine]
where
bannerText =
"Curry Package Manager <curry-language.org/tools/cpm> (version of 01/06/2017)"
"Curry Package Manager <curry-language.org/tools/cpm> (version of 06/06/2017)"
bannerLine = take (length bannerText) (repeat '-')
main :: IO ()
......@@ -99,7 +100,7 @@ runWithArgs opts = do
_ -> do globalCache <- getGlobalCache config repo
case optCommand opts of
Deps -> deps config repo globalCache
PkgInfo o -> info o config repo globalCache
PkgInfo o -> infoCmd o config repo globalCache
Checkout o -> checkout o config repo globalCache
InstallApp o -> installapp o config repo globalCache
Install o -> install o config repo globalCache
......@@ -206,8 +207,10 @@ data CompilerOptions = CompilerOptions
{ comCommand :: String }
data DocOptions = DocOptions
{ docDir :: Maybe String -- documentation directory
, docModules :: Maybe [String] -- modules to be documented
{ docDir :: Maybe String -- documentation directory
, docModules :: Maybe [String] -- modules to be documented
, docPrograms :: Bool -- generate documentation for programs
, docManual :: Bool -- generate manual (if specified)
}
data TestOptions = TestOptions
......@@ -283,7 +286,7 @@ compOpts s = case optCommand s of
docOpts :: Options -> DocOptions
docOpts s = case optCommand s of
Doc opts -> opts
_ -> DocOptions Nothing Nothing
_ -> DocOptions Nothing Nothing True True
testOpts :: Options -> TestOptions
testOpts s = case optCommand s of
......@@ -513,6 +516,18 @@ optionParser = optParser
<> help ("The modules to be documented, " ++
"separate multiple modules by comma")
<> optional )
<.> flag (\a -> Right $ a { optCommand = Doc (docOpts a)
{ docManual = False } })
( short "p"
<> long "programs"
<> help "Generate only program documentation (with CurryDoc)"
<> optional )
<.> flag (\a -> Right $ a { optCommand = Doc (docOpts a)
{ docPrograms = False } })
( short "t"
<> long "text"
<> help "Generate only manual (according to package specification)"
<> optional )
testArgs =
option (\s a -> Right $ a { optCommand = Test (testOpts a)
......@@ -625,22 +640,22 @@ deps cfg repo gc =
resolveDependencies cfg repo gc specDir |>= \result ->
putStrLn (showResult result) >> succeedIO ()
info :: InfoOptions -> Config -> Repository -> GlobalCache
infoCmd :: InfoOptions -> Config -> Repository -> GlobalCache
-> IO (ErrorLogger ())
info (InfoOptions Nothing Nothing allinfos plain) _ _ gc =
infoCmd (InfoOptions Nothing Nothing allinfos plain) _ _ gc =
tryFindLocalPackageSpec "." |>= \specDir ->
loadPackageSpec specDir |>= printInfo allinfos plain gc
info (InfoOptions (Just pkg) Nothing allinfos plain) cfg repo gc =
infoCmd (InfoOptions (Just pkg) Nothing allinfos plain) cfg repo gc =
case findLatestVersion cfg repo pkg False of
Nothing -> failIO $
"Package '" ++ pkg ++ "' not found in package repository."
Just p -> printInfo allinfos plain gc p
info (InfoOptions (Just pkg) (Just v) allinfos plain) _ repo gc =
infoCmd (InfoOptions (Just pkg) (Just v) allinfos plain) _ repo gc =
case findVersion repo pkg v of
Nothing -> failIO $ "Package '" ++ pkg ++ "-" ++ (showVersion v) ++
Nothing -> failIO $ "Package '" ++ pkg ++ "-" ++ showVersion v ++
"' not found in package repository."
Just p -> printInfo allinfos plain gc p
info (InfoOptions Nothing (Just _) _ _) _ _ _ =
infoCmd (InfoOptions Nothing (Just _) _ _) _ _ _ =
failIO "Must specify package name"
printInfo :: Bool -> Bool -> GlobalCache -> Package
......@@ -952,6 +967,7 @@ addCmd (AddOptions pkgdir force) config = do
useForce = "Use option '-f' or '--force' to overwrite it."
------------------------------------------------------------------------------
--- `doc` command: run `curry doc` on the modules provided as an argument
--- or, if they are not given, on exported modules (if specified in the
--- package), on the main executable (if specified in the package),
......@@ -961,33 +977,91 @@ docCmd :: DocOptions -> Config -> IO (Repository,GlobalCache)
docCmd opts cfg getRepoGC =
tryFindLocalPackageSpec "." |>= \specDir ->
loadPackageSpec specDir |>= \pkg -> do
checkCompiler cfg pkg
let docdir = maybe "cdoc" id (docDir opts)
exports = exportedModules pkg
mainmod = maybe Nothing
(\ (PackageExecutable _ emain _) -> Just emain)
(executableSpec pkg)
(docmods,apidoc) <-
maybe (if null exports
then maybe (curryModulesInDir (specDir </> "src") >>=
\ms -> return (ms,True))
(\m -> return ([m],False))
mainmod
else return (exports,True))
(\ms -> return (ms,True))
(docModules opts)
if null docmods
then putStrLn "No modules to be documented!" >> succeedIO ()
else
if apidoc
then foldEL (\_ -> docModule specDir docdir) () docmods |>
runDocCmd specDir
([currydoc, "--title", apititle pkg, "--onlyindexhtml",
docdir] ++ docmods) |>
log Info ("Documentation generated in '"++docdir++"'")
else runDocCmd specDir [currydoc, docdir, head docmods]
let docdir = maybe "cdoc" id (docDir opts)
absdocdir <- getAbsolutePath docdir
createDirectoryIfMissing True absdocdir
(if docManual opts then genPackageManual opts cfg getRepoGC pkg absdocdir
else succeedIO ()) |>
(if docPrograms opts then genDocForPrograms opts cfg getRepoGC specDir pkg
else succeedIO ())
--- Generate manual according to documentation specification of package.
genPackageManual :: DocOptions -> Config -> IO (Repository,GlobalCache)
-> Package -> String -> IO (ErrorLogger ())
genPackageManual _ _ _ pkg outputdir = case documentation pkg of
Nothing -> succeedIO ()
Just (PackageDocumentation docdir docmain doccmd) -> do
let formatcmd = replaceSubString "OUTDIR" outputdir $
if null doccmd then formatCmd docmain
else doccmd
if null formatcmd
then infoMessage $ "Cannot format documentation file '" ++
docmain ++ "' (unknown kind)"
else do
debugMessage $ "Executing command: " ++ formatcmd
inDirectory docdir $ system formatcmd
let outfile = outputdir </> replaceExtension docmain ".pdf"
system ("chmod -f 644 " ++ quote outfile) -- make it readable
infoMessage $ "Package documentation written to '" ++ outfile ++ "'."
succeedIO ()
where
formatCmd docmain
| ".tex" `isSuffixOf` docmain
= let formatcmd = "pdflatex -output-directory=\"OUTDIR\" " ++ docmain
in formatcmd ++ " && " ++ formatcmd
| ".md" `isSuffixOf` docmain
= "pandoc " ++ docmain ++
" -o \"OUTDIR" </> replaceExtension docmain ".pdf" ++ "\""
| otherwise = ""
--- Replace every occurrence of the first argument by the second argument
--- in a string (third argument).
replaceSubString :: String -> String -> String -> String
replaceSubString sub newsub s = replString s
where
apititle pkg = "\"API Documentation of Package '" ++ name pkg ++ "'\""
sublen = length sub
replString [] = []
replString ccs@(c:cs) =
if take sublen ccs == sub
then newsub ++ replString (drop sublen ccs)
else c : replString cs
--- Generate program documentation:
--- run `curry doc` on the modules provided as an argument
--- or, if they are not given, on exported modules (if specified in the
--- package), on the main executable (if specified in the package),
--- or on all source modules of the package.
genDocForPrograms :: DocOptions -> Config -> IO (Repository,GlobalCache)
-> String -> Package -> IO (ErrorLogger ())
genDocForPrograms opts cfg getRepoGC specDir pkg = do
checkCompiler cfg pkg
let docdir = maybe "cdoc" id (docDir opts)
exports = exportedModules pkg
mainmod = maybe Nothing
(\ (PackageExecutable _ emain _) -> Just emain)
(executableSpec pkg)
(docmods,apidoc) <-
maybe (if null exports
then maybe (curryModulesInDir (specDir </> "src") >>=
\ms -> return (ms,True))
(\m -> return ([m],False))
mainmod
else return (exports,True))
(\ms -> return (ms,True))
(docModules opts)
if null docmods
then putStrLn "No modules to be documented!" >> succeedIO ()
else
if apidoc
then foldEL (\_ -> docModule specDir docdir) () docmods |>
runDocCmd specDir
([currydoc, "--title", apititle, "--onlyindexhtml",
docdir] ++ docmods) |>
log Info ("Documentation generated in '"++docdir++"'")
else runDocCmd specDir [currydoc, docdir, head docmods]
where
apititle = "\"API Documentation of Package '" ++ name pkg ++ "'\""
currydoc = curryExec cfg ++ " doc"
......@@ -999,6 +1073,8 @@ docCmd opts cfg getRepoGC =
infoMessage $ "Running CurryDoc: " ++ cmd
execWithPkgDir (ExecOptions cmd []) cfg getRepoGC pkgdir
------------------------------------------------------------------------------
--- `test` command: run `curry check` on the modules provided as an argument
--- or, if they are not provided, on the exported (if specified)
--- or all source modules of the package.
......
......@@ -30,7 +30,7 @@ module CPM.Package
, PackageId (..)
, PackageSource (..)
, GitRevision (..)
, PackageExecutable (..), PackageTest (..)
, PackageExecutable (..), PackageTest (..), PackageDocumentation (..)
, showDependency
, showCompilerDependency
, loadPackageSpec
......@@ -118,6 +118,15 @@ data PackageExecutable = PackageExecutable String String [(String,String)]
data PackageTest = PackageTest String [String] String String
deriving (Eq,Show)
--- The specification to generate the documentation of the package.
--- It consists of the name of the directory containing the documentation,
--- a main file (usually, a LaTeX file) containing the documentation,
--- and a command to generate the documentation. If the command is missing
--- and the main file has the suffix "tex", e.g., "manual.tex",
--- the default command is "pdflatex manual.tex".
data PackageDocumentation = PackageDocumentation String String String
deriving (Eq,Show)
--- 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
......@@ -162,6 +171,7 @@ data Package = Package {
, configModule :: Maybe String
, executableSpec :: Maybe PackageExecutable
, testSuite :: Maybe [PackageTest]
, documentation :: Maybe PackageDocumentation
}
deriving (Eq,Show)
......@@ -189,6 +199,7 @@ emptyPackage = Package {
, configModule = Nothing
, executableSpec = Nothing
, testSuite = Nothing
, documentation = Nothing
}
--- Translates a package to a JSON object.
......@@ -214,7 +225,8 @@ packageSpecToJSON pkg = JObject $
stringListToJSON "exportedModules" (exportedModules pkg) ++
maybeStringToJSON "configModule" (configModule pkg) ++
maybeExecToJSON (executableSpec pkg) ++
maybeTestToJSON (testSuite pkg)
maybeTestToJSON (testSuite pkg) ++
maybeDocuToJSON (documentation pkg)
where
dependenciesToJSON deps = JObject $ map dependencyToJSON deps
where dependencyToJSON (Dependency p vc) =
......@@ -262,6 +274,15 @@ packageSpecToJSON pkg = JObject $
stringListToJSON "modules" mods ++
(if null script then [] else [("script", JString script)])
maybeDocuToJSON =
maybe [] (\ (PackageDocumentation docdir docmain doccmd) ->
[("documentation",
JObject $ [ ("src-dir", JString docdir)
, ("main", JString docmain)] ++
if null doccmd
then []
else [("command", JString doccmd)] )])
stringListToJSON fname exps =
if null exps then []
else [(fname, JArray $ map JString exps)]
......@@ -297,7 +318,7 @@ loadPackageSpec dir = do
--- @param p1 the first package
--- @param p2 the second package
packageIdEq :: Package -> Package -> Bool
packageIdEq p1 p2 = (name p1) == (name p2) && (version p1) == (version p2)
packageIdEq p1 p2 = name p1 == name p2 && version p1 == version p2
--- Shows the package source in human-readable format.
showPackageSource :: Package -> String
......@@ -465,6 +486,7 @@ packageSpecFromJObject kv =
getCompilerCompatibility $ \compilerCompatibility ->
getExecutableSpec $ \executable ->
getTestSuite $ \testsuite ->
getDocumentationSpec $ \docspec ->
Right Package {
name = name
, version = version
......@@ -487,6 +509,7 @@ packageSpecFromJObject kv =
, configModule = configModule
, executableSpec = executable
, testSuite = testsuite
, documentation = docspec
}
where
mustBeVersion :: String -> (Version -> Either String a) -> Either String a
......@@ -581,6 +604,20 @@ packageSpecFromJObject kv =
Just JNull -> Left $ "Expected an object, got 'null'" ++ forKey
where forKey = " for key 'testsuite'"
getDocumentationSpec :: (Maybe PackageDocumentation -> Either String a)
-> Either String a
getDocumentationSpec f = case lookup "documentation" kv of
Nothing -> f Nothing
Just (JObject s) -> case docuSpecFromJObject s of Left e -> Left e
Right s' -> f (Just s')
Just (JString _) -> Left $ "Expected an object, got a string" ++ forKey
Just (JArray _) -> Left $ "Expected an object, got an array" ++ forKey
Just (JNumber _) -> Left $ "Expected an object, got a number" ++ forKey
Just JTrue -> Left $ "Expected an object, got 'true'" ++ forKey
Just JFalse -> Left $ "Expected an object, got 'false'" ++ forKey
Just JNull -> Left $ "Expected an object, got 'null'" ++ forKey
where forKey = " for key 'documentation'"
mandatoryString :: String -> [(String, JValue)]
-> (String -> Either String a) -> Either String a
......@@ -778,6 +815,15 @@ getOptStringList optional key kv = case lookup (key++"s") kv of
where
forKey = " for key '" ++ key ++ "s'"
--- Reads documentation specification from the key-value-pairs of a JObject.
docuSpecFromJObject :: [(String, JValue)] -> Either String PackageDocumentation
docuSpecFromJObject kv =
mandatoryString "src-dir" kv $ \docdir ->
mandatoryString "main" kv $ \docmain ->
optionalString "command" kv $ \doccmd ->
Right $ PackageDocumentation docdir docmain (maybe "" id doccmd)
--- 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.
......
......@@ -43,7 +43,7 @@ import CPM.Package ( Package (..)
, showVersion, PackageSource (..), showDependency
, showCompilerDependency, showPackageSource
, Dependency, GitRevision (..), PackageExecutable (..)
, PackageTest (..)
, PackageTest (..), PackageDocumentation (..)
, packageIdEq, loadPackageSpec)
import CPM.Resolution
......@@ -212,7 +212,8 @@ renderPackageInfo allinfos plain gc pkg = pPrint doc
, cats, deps, compilers, descr ] ++
if allinfos
then [ srcdirs, expmods, cfgmod, execspec] ++ testsuites ++
[ src, licns, licfl, copyrt, homepg, reposy, bugrep]
[ docuspec, src, licns, licfl, copyrt, homepg
, reposy, bugrep]
else []
pkgId = packageId pkg
......@@ -273,6 +274,16 @@ renderPackageInfo allinfos plain gc pkg = pPrint doc
align (fillSep (map text mods)))))
tests
docuspec = case documentation pkg of
Nothing -> empty
Just (PackageDocumentation docdir docmain doccmd) ->
boldText "Documentation" <$$>
indent 4 (boldText "Directory " <+> text docdir) <$$>
indent 4 (boldText "Main file " <+> text docmain) <$$>
if null doccmd
then empty
else indent 4 (boldText "Command ") <+> text doccmd
descr = showParaField description "Description"
licns = showLineField license "License"
licfl = showLineField licenseFile "License file"
......
......@@ -20,20 +20,22 @@ module CPM.Repository
, updateRepositoryCache
) where
import Char ( toLower )
import Char ( toLower )
import Directory
import Either
import FilePath
import IO
import List
import ReadShowTerm ( readQTermFile, writeQTermFile )
import System ( exitWith )
import ReadShowTerm ( showQTerm, readQTerm )
import System ( exitWith )
import CPM.Config ( Config, repositoryDir, packageIndexRepository )
import CPM.Config ( Config, repositoryDir, packageIndexRepository )
import CPM.ConfigPackage ( packageVersion )
import CPM.ErrorLogger
import CPM.Package
import CPM.FileUtil ( checkAndGetDirectoryContents, inDirectory
, whenFileExists )
import CPM.Resolution ( isCompatibleToCompiler )
import CPM.FileUtil ( checkAndGetDirectoryContents, inDirectory
, whenFileExists )
import CPM.Resolution ( isCompatibleToCompiler )
data Repository = Repository [Package]
......@@ -123,7 +125,7 @@ readRepository cfg = do
mbrepo <- readRepositoryCache cfg
case mbrepo of
Nothing -> do
infoMessage "Writing the repository cache..."
infoMessage "Writing repository cache..."
(repo, repoErrors) <- readRepositoryFrom (repositoryDir cfg)
if null repoErrors
then writeRepositoryCache cfg repo >> return repo
......@@ -157,7 +159,7 @@ readRepositoryFrom path = do
Right s -> Right s
dirOrSpec d = (not $ isPrefixOf "." d) && takeExtension d /= ".md" &&
d /= repositoryCacheFileName
(not $ isPrefixOf repositoryCacheFileName d)
--- Updates the package index from the central Git repository.
updateRepository :: Config -> IO (ErrorLogger ())
......@@ -203,7 +205,8 @@ updateRepositoryCache cfg = do
--- Stores the given repository in the cache.
writeRepositoryCache :: Config -> Repository -> IO ()
writeRepositoryCache cfg repo =
writeQTermFile (repositoryCache cfg) repo
writeFile (repositoryCache cfg)
(packageVersion ++ "\n" ++ showQTerm repo)
--- Reads the given repository from the cache.
readRepositoryCache :: Config -> IO (Maybe Repository)
......@@ -212,9 +215,20 @@ readRepositoryCache cfg = do
excache <- doesFileExist cf
if excache
then debugMessage ("Reading repository cache from '" ++ cf ++ "'...") >>
catch (readQTermFile cf >>= \t -> return $!! Just t)
(\_ -> cleanRepositoryCache cfg >> return Nothing)
catch (readTermInCacheFile cf)
(\_ -> do infoMessage "Cleaning broken repository cache..."
cleanRepositoryCache cfg
return Nothing )
else return Nothing
where
readTermInCacheFile cf = do
h <- openFile cf ReadMode
pv <- hGetLine h
if pv == packageVersion
then hGetContents h >>= \t -> return $!! Just (readQTerm t)
else do infoMessage "Cleaning repository cache (wrong version)..."
cleanRepositoryCache cfg
return Nothing
--- Cleans the repository cache.
cleanRepositoryCache :: Config -> IO ()
......
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