Commit 14ef157f authored by Michael Hanus 's avatar Michael Hanus
Browse files

CPM updated

parent d9f9d732
......@@ -820,22 +820,6 @@ Checks out a specific version of a package
into the local directory \code{$package$}
in order to test its operations or install a binary of the package..
% \item[\fbox{\code{installapp $package$ [--$pre$]}}]
% Install the application provided by the newest version
% (compatible to the current compiler) of a package.
% The binary of the application is installed into the directory
% \code{\$HOME/.cpm/bin}
% (this location can be changed via the \code{\$HOME/.cpmrc} configuration file
% or by the CPM option \code{--define}, see Section~\ref{sec:config}).
% \code{--$pre$} enables the installation of pre-release versions.
%
% \item[\fbox{\code{installapp $package$ $version$}}]
% Install the application provided by a specific version of a package.
% The binary of the application is installed into the directory
% \code{\$HOME/.cpm/bin}
% (this location can be changed via the \code{\$HOME/.cpmrc} configuration file
% or by the CPM option \code{--define}, see Section~\ref{sec:config}).
\item[\fbox{\code{upgrade}}]
Upgrades all dependencies of the current package to
the newest compatible version.
......@@ -848,6 +832,8 @@ versions.
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.
Using the option \code{--path]}, only the value of \code{CURRYPATH} required
to load modules of this package is shown.
\item[\fbox{\code{test}}]
Tests the current package with CurryCheck.
......
......@@ -8,24 +8,24 @@ module CPM.Config
( Config ( Config, packageInstallDir, binInstallDir, repositoryDir
, appPackageDir, packageIndexRepository, curryExec
, compilerVersion )
, readConfiguration, readConfigurationWithDefault, defaultConfig
, showCompilerVersion ) where
import Char (toUpper)
import Directory (getHomeDirectory, createDirectoryIfMissing)
import Distribution (installDir, curryCompiler, curryCompilerMinorVersion
, curryCompilerMajorVersion)
import FilePath ((</>))
import Function ((***))
import IOExts (evalCmd)
import List (split, splitOn, intersperse)
import Maybe (mapMaybe)
import PropertyFile (readPropertyFile)
import Read (readInt)
, readConfigurationWith, defaultConfig
, showConfiguration, showCompilerVersion ) where
import Char ( toUpper )
import Directory ( getHomeDirectory, createDirectoryIfMissing )
import Distribution ( installDir, curryCompiler, curryCompilerMinorVersion
, curryCompilerMajorVersion )
import FilePath ( (</>), isAbsolute )
import Function ( (***) )
import IOExts ( evalCmd )
import List ( split, splitOn, intersperse )
import Maybe ( mapMaybe )
import PropertyFile ( readPropertyFile )
import Read ( readInt )
import CPM.ErrorLogger
import CPM.FileUtil (ifFileExists)
import CPM.Helpers (strip)
import CPM.FileUtil ( ifFileExists, getFileInPath )
import CPM.Helpers ( strip )
--- The location of the central package index.
packageIndexURI :: String
......@@ -66,21 +66,51 @@ defaultConfig = Config
curryCompilerMinorVersion)
}
--- Shows the configuration.
showConfiguration :: Config -> String
showConfiguration cfg = unlines
[ "Current configuration:"
, "Compiler version : " ++ showCompilerVersion cfg
, "CURRYBIN : " ++ curryExec cfg
, "REPOSITORYPATH : " ++ repositoryDir cfg
, "PACKAGEINSTALLPATH: " ++ packageInstallDir cfg
, "BININSTALLPATH : " ++ binInstallDir cfg
, "APPPACKAGEPATH : " ++ appPackageDir cfg
]
--- Shows the compiler version in the configuration.
showCompilerVersion :: Config -> String
showCompilerVersion cfg =
let (cname,cmaj,cmin) = compilerVersion cfg
in cname ++ ' ' : show cmaj ++ "." ++ show cmin
--- Sets an existing compiler executable in the configuration.
--- Try to use the predefined CURRYBIN value.
--- If it is an absolute path name but does not exists,
--- try to find the executable "curry" in the path.
setCompilerExecutable :: Config -> IO Config
setCompilerExecutable cfg = do
let exec = curryExec cfg
if isAbsolute exec
then ifFileExists exec (return cfg) (findExecutable "curry")
else findExecutable exec
where
findExecutable exec =
getFileInPath exec >>=
maybe (error $ "Executable '" ++ exec ++ "' not found in path!")
(\absexec -> return cfg { curryExec = absexec })
--- Sets the correct compiler version in the configuration.
setCompilerVersion :: Config -> IO Config
setCompilerVersion cfg =
setCompilerVersion cfg0 = do
cfg <- setCompilerExecutable cfg0
if curryExec cfg == installDir </> "bin" </> curryCompiler
then return cfg { compilerVersion = currVersion }
else do (c1,sname,_) <- evalCmd (curryExec cfg) ["--compiler-name"] ""
(c2,svers,_) <- evalCmd (curryExec cfg) ["--numeric-version"] ""
else do (c1,sname,e1) <- evalCmd (curryExec cfg) ["--compiler-name"] ""
(c2,svers,e2) <- evalCmd (curryExec cfg) ["--numeric-version"] ""
when (c1 > 0 || c2 > 0) $
error $ "Cannot determine compiler version"
error $ "Cannot determine compiler version:\n" ++
unlines (filter (not . null) [e1,e2])
let cname = strip sname
cvers = strip svers
(majs:mins:_) = split (=='.') cvers
......@@ -90,24 +120,18 @@ setCompilerVersion cfg =
currVersion = (curryCompiler, curryCompilerMajorVersion,
curryCompilerMinorVersion)
--- Reads the .cpmrc file from the user's home directory (if present) and merges
--- its contents into the default configuration. Resolves the $HOME variable
--- after merging and creates any missing directories.
--- May return an error using Left.
readConfiguration :: IO (Either String Config)
readConfiguration = readConfigurationWithDefault []
--- Reads the .cpmrc file from the user's home directory (if present) and merges
--- its contents and some given default settings into the default configuration.
--- Reads the .cpmrc file from the user's home directory (if present) and
--- merges its contents and some given default settings (first argument)
--- into the configuration used by CPM.
--- Resolves the $HOME variable after merging and creates
--- any missing directories. May return an error using Left.
readConfigurationWithDefault :: [(String,String)] -> IO (Either String Config)
readConfigurationWithDefault defsettings = do
--- any missing directories. May return an error using `Left`.
readConfigurationWith :: [(String,String)] -> IO (Either String Config)
readConfigurationWith defsettings = do
home <- getHomeDirectory
configFile <- return $ home </> ".cpmrc"
settingsFromFile <-
ifFileExists configFile
(readPropertyFile configFile >>= \p -> return $ stripProps p)
(readPropertyFile configFile >>= return . stripProps)
(return [])
let mergedSettings = mergeConfigSettings defaultConfig
(settingsFromFile ++ stripProps defsettings)
......
......@@ -12,7 +12,7 @@ module CPM.FileUtil
, linkTarget
, copyDirectoryFollowingSymlinks
, quote
, fileInPath
, fileInPath, getFileInPath
, tempDir
, inTempDir
, inDirectory
......@@ -25,7 +25,7 @@ module CPM.FileUtil
import Directory ( doesFileExist, doesDirectoryExist, getCurrentDirectory
, setCurrentDirectory, getDirectoryContents
, getTemporaryDirectory, doesDirectoryExist, createDirectory
, createDirectoryIfMissing)
, createDirectoryIfMissing, getAbsolutePath )
import System ( system, getEnviron, exitWith )
import IOExts ( evalCmd, readCompleteFile )
import FilePath ( FilePath, replaceFileName, (</>), searchPathSeparator )
......@@ -85,9 +85,23 @@ quote s = "\"" ++ s ++ "\""
fileInPath :: String -> IO Bool
fileInPath file = do
path <- getEnviron "PATH"
dirs <- return $ splitOn ":" path
let dirs = splitOn ":" path
(liftIO (any id)) $ mapIO (doesFileExist . (</> file)) dirs
--- Checks whether a file exists in one of the directories on the PATH
--- and returns absolute path, otherwise returns `Nothing`.
getFileInPath :: String -> IO (Maybe String)
getFileInPath file = do
path <- getEnviron "PATH"
checkPath (splitOn ":" path)
where
checkPath [] = return Nothing
checkPath (dir:dirs) = do
let dirfile = dir </> file
ifFileExists dirfile
(getAbsolutePath dirfile >>= return . Just)
(checkPath dirs)
--- Gets CPM's temporary directory.
tempDir :: IO String
tempDir = do
......
......@@ -28,7 +28,8 @@ import CPM.FileUtil ( fileInPath, joinSearchPath, safeReadFile, whenFileExists
, ifFileExists, inDirectory, removeDirectoryComplete
, copyDirectory, quote )
import CPM.Config ( Config (..)
, readConfigurationWithDefault, showCompilerVersion )
, readConfigurationWith, showCompilerVersion
, showConfiguration )
import CPM.PackageCache.Global ( GlobalCache, readInstalledPackagesFromDir
, installFromZip, checkoutPackage
, uninstallPackage )
......@@ -48,7 +49,7 @@ cpmBanner :: String
cpmBanner = unlines [bannerLine,bannerText,bannerLine]
where
bannerText =
"Curry Package Manager <curry-language.org/tools/cpm> (version of 25/10/2017)"
"Curry Package Manager <curry-language.org/tools/cpm> (version of 03/11/2017)"
bannerLine = take (length bannerText) (repeat '-')
main :: IO ()
......@@ -76,22 +77,24 @@ runWithArgs opts = do
"(they are required for CPM to work):\n" ++
intercalate ", " missingExecutables
exitWith 1
config <- readConfigurationWithDefault (optDefConfig opts) >>= \c ->
config <- readConfigurationWith (optDefConfig opts) >>= \c ->
case c of
Left err -> do putStrLn $ "Error reading .cpmrc settings: " ++ err
exitWith 1
Right c' -> return c'
setLogLevel $ optLogLevel opts
debugMessage (showConfiguration config)
let getRepoGC = readRepository config >>= \repo ->
getGlobalCache config repo >>= \gc -> return (repo,gc)
setLogLevel $ optLogLevel opts
(msgs, result) <- case optCommand opts of
NoCommand -> failIO "NoCommand"
Update -> updateRepository config
Compiler o -> compiler o config getRepoGC
Exec o -> exec o config getRepoGC
Exec o -> execCmd o config getRepoGC
Doc o -> docCmd o config getRepoGC
Test o -> testCmd o config getRepoGC
Uninstall o -> uninstall o config getRepoGC
Deps o -> depsCmd o config getRepoGC
Link o -> linkCmd o config
Add o -> addCmd o config
Clean -> cleanPackage Info
......@@ -102,7 +105,6 @@ runWithArgs opts = do
Search o -> searchCmd o config repo
_ -> do globalCache <- getGlobalCache config repo
case optCommand opts of
Deps -> depsCmd config repo globalCache
PkgInfo o -> infoCmd o config repo globalCache
Checkout o -> checkout o config repo globalCache
InstallApp o -> installapp o config repo globalCache
......@@ -132,14 +134,14 @@ data Options = Options
, optCommand :: Command }
data Command
= Deps
| NoCommand
= NoCommand
| Deps DepsOptions
| Checkout CheckoutOptions
| InstallApp CheckoutOptions
| Install InstallOptions
| Uninstall UninstallOptions
| PkgInfo InfoOptions
| Compiler CompilerOptions
| Compiler ExecOptions
| Update
| List ListOptions
| Search SearchOptions
......@@ -153,6 +155,10 @@ data Command
| New NewOptions
| Clean
data DepsOptions = DepsOptions
{ depsPath :: Bool -- show CURRYPATH only?
}
data CheckoutOptions = CheckoutOptions
{ coPackage :: String
, coVersion :: Maybe Version
......@@ -205,13 +211,9 @@ data NewOptions = NewOptions
{ projectName :: String }
data ExecOptions = ExecOptions
{ exeCommand :: String -- command to be executed
, exePath :: [String] -- additional load path
{ exeCommand :: String -- the command to be executed
}
data CompilerOptions = CompilerOptions
{ comCommand :: String }
data DocOptions = DocOptions
{ docDir :: Maybe String -- documentation directory
, docModules :: Maybe [String] -- modules to be documented
......@@ -229,6 +231,11 @@ data DiffOptions = DiffOptions
, diffBehavior :: Bool
, diffUseAna :: Bool }
depsOpts :: Options -> DepsOptions
depsOpts s = case optCommand s of
Deps opts -> opts
_ -> DepsOptions False
checkoutOpts :: Options -> CheckoutOptions
checkoutOpts s = case optCommand s of
Checkout opts -> opts
......@@ -282,12 +289,7 @@ newOpts s = case optCommand s of
execOpts :: Options -> ExecOptions
execOpts s = case optCommand s of
Exec opts -> opts
_ -> ExecOptions "" []
compOpts :: Options -> CompilerOptions
compOpts s = case optCommand s of
Compiler opts -> opts
_ -> CompilerOptions ""
_ -> ExecOptions ""
docOpts :: Options -> DocOptions
docOpts s = case optCommand s of
......@@ -368,16 +370,17 @@ optionParser allargs = optParser
Right
(checkoutArgs InstallApp)
<|> command "deps" (help "Calculate dependencies")
(\a -> Right $ a { optCommand = Deps }) []
(\a -> Right $ a { optCommand = Deps (depsOpts a) })
depsArgs
<|> command "clean" (help "Clean the current package")
(\a -> Right $ a { optCommand = Clean }) []
(\a -> Right $ a { optCommand = Clean }) []
<|> command "new" (help "Create a new package") Right newArgs
<|> command "update" (help "Update the package index")
(\a -> Right $ a { optCommand = Update }) []
<|> command "curry"
(help "Load package spec and start Curry with correct dependencies.")
(\a -> Right $ a { optCommand = Compiler (compOpts a) })
curryArgs
(\a -> Right $ a { optCommand = Compiler (execOpts a) })
curryArgs
<|> command "exec"
(help "Execute a command with the CURRYPATH set")
(\a -> Right $ a { optCommand = Exec (execOpts a) })
......@@ -406,9 +409,19 @@ optionParser allargs = optParser
upgradeArgs
<|> command "link" (help "Link a package to the local cache") Right
linkArgs
<|> command "add" (help "Add a package (as dependency or to the local repository)") Right
addArgs ) )
<|> command "add"
(help "Add a package (as dependency or to the local repository)")
Right
addArgs ) )
where
depsArgs =
flag (\a -> Right $ a { optCommand = Deps (depsOpts a)
{ depsPath = True } })
( short "p"
<> long "path"
<> help "Show value of CURRYPATH only"
<> optional )
checkoutArgs cmd =
arg (\s a -> Right $ a { optCommand = cmd (checkoutOpts a)
{ coPackage = s } })
......@@ -473,8 +486,8 @@ optionParser allargs = optParser
<> help "The name of the new project" )
curryArgs =
rest (\_ a -> Right $ a { optCommand = Compiler (compOpts a)
{ comCommand = unwords remargs } })
rest (\_ a -> Right $ a { optCommand = Compiler (execOpts a)
{ exeCommand = unwords remargs } })
( metavar "ARGS"
<> help "The options to pass to the compiler"
<> optional )
......@@ -485,7 +498,7 @@ optionParser allargs = optParser
rest (\_ a -> Right $ a { optCommand = Exec (execOpts a)
{ exeCommand = unwords remargs } })
( metavar "CMD"
<> help "The command to execute. Don't forget the quotes!"
<> help "The command to be executed."
<> optional )
where
remargs = tail (snd (break (=="exec") allargs))
......@@ -664,11 +677,20 @@ checkExecutables = do
, "ln"
, "readlink" ]
depsCmd :: Config -> Repository -> GlobalCache -> IO (ErrorLogger ())
depsCmd cfg repo gc =
depsCmd :: DepsOptions -> Config -> IO (Repository,GlobalCache)
-> IO (ErrorLogger ())
depsCmd opts cfg getRepoGC =
getLocalPackageSpec "." |>= \specDir ->
resolveDependencies cfg repo gc specDir |>= \result ->
putStrLn (showResult result) >> succeedIO ()
loadPackageSpec specDir |>= \pkg ->
checkCompiler cfg pkg >>
if depsPath opts -- show CURRYPATH only?
then loadCurryPathFromCache specDir |>=
maybe (computePackageLoadPath cfg specDir getRepoGC)
succeedIO |>= \currypath ->
putStrLn currypath >> succeedIO ()
else getRepoGC >>= \ (repo,gc) ->
resolveDependencies cfg repo gc specDir |>= \result ->
putStrLn (showResult result) >> succeedIO ()
infoCmd :: InfoOptions -> Config -> Repository -> GlobalCache
-> IO (ErrorLogger ())
......@@ -694,33 +716,6 @@ printInfo allinfos plain gc pkg =
putStrLn (renderPackageInfo allinfos plain gc pkg) >> succeedIO ()
compiler :: CompilerOptions -> Config -> IO (Repository,GlobalCache)
-> IO (ErrorLogger ())
compiler o cfg getRepoGC =
getLocalPackageSpec "." |>= \pkgdir ->
loadPackageSpec pkgdir |>= \pkg ->
checkCompiler cfg pkg >>
loadCurryPathFromCache pkgdir |>=
maybe (computePackageLoadPath pkgdir pkg) succeedIO |>= \currypath ->
log Info ("Starting '" ++ currybin ++ "' with") |>
log Info ("CURRYPATH=" ++ currypath) |>
do setEnviron "CURRYPATH" $ currypath
ecode <- showExecCmd $ currybin ++ " " ++ comCommand o
unsetEnviron "CURRYPATH"
unless (ecode==0) (exitWith ecode)
succeedIO ()
where
currybin = curryExec cfg
computePackageLoadPath pkgdir pkg =
getRepoGC >>= \ (repo,gc) ->
resolveAndCopyDependenciesForPackage cfg repo gc pkgdir pkg |>= \pkgs ->
getAbsolutePath pkgdir >>= \abs -> succeedIO () |>
let srcdirs = map (abs </>) (sourceDirsOf pkg)
currypath = joinSearchPath (srcdirs ++ dependencyPathsSeparate pkgs abs)
in saveCurryPathToCache pkgdir currypath >> succeedIO currypath
checkout :: CheckoutOptions -> Config -> Repository -> GlobalCache
-> IO (ErrorLogger ())
checkout (CheckoutOptions pkg Nothing pre) cfg repo gc =
......@@ -791,7 +786,7 @@ installapp opts cfg repo gc = do
checkCompiler :: Config -> Package -> IO ()
checkCompiler cfg pkg =
unless (isCompatibleToCompiler cfg pkg)
(error $ "Incompatible compiler: " ++ showCompilerVersion cfg)
(error $ "Compiler incompatible to package: " ++ showCompilerVersion cfg)
--- Installs the executable specified in the package in the
--- bin directory of CPM (compare .cpmrc).
......@@ -813,7 +808,7 @@ installExecutable cfg repo pkg =
, ":load", mainmod, ":save", ":quit"]
bindir = binInstallDir cfg
binexec = bindir </> name
in compiler CompilerOptions { comCommand = cmd }
in compiler (ExecOptions cmd)
cfg (return (repo,gc)) |>
log Info ("Installing executable '" ++ name ++ "' into '" ++
bindir ++ "'") |>
......@@ -1176,7 +1171,7 @@ genDocForPrograms opts cfg getRepoGC specDir pkg = do
runDocCmd pkgdir doccmd = do
let cmd = unwords doccmd
infoMessage $ "Running CurryDoc: " ++ cmd
execWithPkgDir (ExecOptions cmd []) cfg getRepoGC pkgdir
execWithPkgDir (ExecOptions cmd) cfg getRepoGC pkgdir
------------------------------------------------------------------------------
......@@ -1216,7 +1211,7 @@ testCmd opts cfg getRepoGC =
debugMessage $ "Removing directory: " ++ currysubdir
showExecCmd (unwords ["rm", "-rf", currysubdir])
inDirectory (apkgdir </> dir) $
execWithPkgDir (ExecOptions testcmd []) cfg getRepoGC apkgdir
execWithPkgDir (ExecOptions testcmd) cfg getRepoGC apkgdir
testsuites spec mainprogs = case testModules opts of
Nothing -> maybe (let exports = exportedModules spec
......@@ -1290,33 +1285,44 @@ diff opts cfg repo gc =
(diffModules opts)
else succeedIO ()
-- Implementation of the "curry" command.
compiler :: ExecOptions -> Config -> IO (Repository,GlobalCache)
-> IO (ErrorLogger ())
compiler o cfg getRepoGC =
getLocalPackageSpec "." |>= \pkgdir ->
loadPackageSpec pkgdir |>= \pkg ->
checkCompiler cfg pkg >>
execWithPkgDir (ExecOptions $ curryExec cfg ++ " " ++ exeCommand o)
cfg getRepoGC pkgdir
exec :: ExecOptions -> Config -> IO (Repository,GlobalCache)
-> IO (ErrorLogger ())
exec o cfg getRepoGC =
execCmd :: ExecOptions -> Config -> IO (Repository,GlobalCache)
-> IO (ErrorLogger ())
execCmd o cfg getRepoGC =
getLocalPackageSpec "." |>= execWithPkgDir o cfg getRepoGC
execWithPkgDir :: ExecOptions -> Config -> IO (Repository,GlobalCache)
-> String -> IO (ErrorLogger ())
execWithPkgDir o cfg getRepoGC specDir =
loadCurryPathFromCache specDir |>=
maybe (computePackageLoadPath specDir) succeedIO |>= \currypath ->
let execpath = joinSearchPath (exePath o ++ splitSearchPath currypath)
in log Debug ("Setting CURRYPATH to " ++ execpath) |>
do setEnviron "CURRYPATH" execpath
maybe (computePackageLoadPath cfg specDir getRepoGC)
succeedIO |>= \currypath ->
log Debug ("Setting CURRYPATH to " ++ currypath) |>
do setEnviron "CURRYPATH" currypath
ecode <- showExecCmd (exeCommand o)
unsetEnviron "CURRYPATH"
unless (ecode==0) (exitWith ecode)
succeedIO ()
where
computePackageLoadPath pkgdir =
getRepoGC >>= \ (repo,gc) ->
loadPackageSpec pkgdir |>= \pkg ->
resolveAndCopyDependenciesForPackage cfg repo gc pkgdir pkg |>= \pkgs ->
getAbsolutePath pkgdir >>= \abs -> succeedIO () |>
let srcdirs = map (abs </>) (sourceDirsOf pkg)
currypath = joinSearchPath (srcdirs ++ dependencyPathsSeparate pkgs abs)
in saveCurryPathToCache pkgdir currypath >> succeedIO currypath
computePackageLoadPath :: Config -> String -> IO (Repository,GlobalCache)
-> IO (ErrorLogger String)
computePackageLoadPath cfg pkgdir getRepoGC =
getRepoGC >>= \ (repo,gc) ->
loadPackageSpec pkgdir |>= \pkg ->
resolveAndCopyDependenciesForPackage cfg repo gc pkgdir pkg |>= \pkgs ->
getAbsolutePath pkgdir >>= \abs -> succeedIO () |>
let srcdirs = map (abs </>) (sourceDirsOf pkg)
currypath = joinSearchPath (srcdirs ++ dependencyPathsSeparate pkgs abs)
in saveCurryPathToCache pkgdir currypath >> succeedIO currypath
-- Clean auxiliary files in the current package
......@@ -1389,7 +1395,7 @@ newPackage (NewOptions pname) = do
--- The name of the cache file in a package directory.
curryPathCacheFile :: String -> String
curryPathCacheFile pkgdir = pkgdir </> ".cpm" </> "currypath_cache"
curryPathCacheFile pkgdir = pkgdir </> ".cpm" </> "CURRYPATH_CACHE"
--- Saves package CURRYPATH in local cache file in the given package dir.
saveCurryPathToCache :: String -> String -> IO ()
......
......@@ -26,10 +26,10 @@ check5 = match ``regex [a-z]+''
-- Examples with parameterized regular expressions:
pregexp1 :: a -> a -> [a] -> Bool
pregexp1 :: Ord a => a -> a -> [a] -> Bool
pregexp1 v1 v2 = match ``regex [<v1>-<v2>]*''
pregexp2 :: a -> a -> [a] -> Bool
pregexp2 :: Ord a => a -> a -> [a] -> Bool
pregexp2 v1 v2 = match ``regex (<v1>|<v2>)*''
-- A regular expression containing a complex Curry expression:
......
......@@ -21,7 +21,7 @@ if [ ! -x "$ERD2CURRY" ] ; then
if [ ! -x "$ERD2CURRY" ] ; then
echo "SQL integration not tested: no executable 'erd2curry' found!"
echo "To run the SQL integration test, install 'erd2curry' by:"
echo "> cpm installbin ertools"
echo "> cypm install ertools"
exit
fi
fi
......
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