purescript-0.10.2: psc-package/Main.hs
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TupleSections #-}
module Main where
import qualified Data.Aeson as Aeson
import Data.Aeson.Encode.Pretty
import Data.Foldable (fold, for_, traverse_)
import Data.List (nub)
import qualified Data.Map as Map
import Data.Maybe (mapMaybe)
import qualified Data.Set as Set
import Data.Text (pack)
import qualified Data.Text.Lazy as TL
import qualified Data.Text.Lazy.Builder as TB
import Data.Text.Encoding (encodeUtf8)
import Data.Traversable (for)
import Data.Version (showVersion)
import qualified Filesystem.Path.CurrentOS as Path
import GHC.Generics (Generic)
import qualified Options.Applicative as Opts
import qualified Paths_purescript as Paths
import qualified System.IO as IO
import Turtle hiding (fold)
packageFile :: Path.FilePath
packageFile = "psc-package.json"
data PackageConfig = PackageConfig
{ name :: Text
, depends :: [Text]
, set :: Text
, source :: Text
} deriving (Show, Generic, Aeson.FromJSON, Aeson.ToJSON)
pathToTextUnsafe :: Turtle.FilePath -> Text
pathToTextUnsafe = either (error "Path.toText failed") id . Path.toText
defaultPackage :: Text -> PackageConfig
defaultPackage pkgName =
PackageConfig { name = pkgName
, depends = [ "prelude" ]
, set = "psc-" <> pack (showVersion Paths.version)
, source = "https://github.com/purescript/package-sets.git"
}
readPackageFile :: IO PackageConfig
readPackageFile = do
exists <- testfile packageFile
unless exists $ do
echo "psc-package.json does not exist"
exit (ExitFailure 1)
mpkg <- Aeson.decodeStrict . encodeUtf8 <$> readTextFile packageFile
case mpkg of
Nothing -> do
echo "Unable to parse psc-package.json"
exit (ExitFailure 1)
Just pkg -> return pkg
encodePrettyToText :: Aeson.ToJSON json => json -> Text
encodePrettyToText =
TL.toStrict
. TB.toLazyText
. encodePrettyToTextBuilder' config
where
config = defConfig
{ confCompare =
keyOrder [ "name"
, "set"
, "source"
, "depends"
]
}
writePackageFile :: PackageConfig -> IO ()
writePackageFile =
writeTextFile packageFile
. encodePrettyToText
data PackageInfo = PackageInfo
{ repo :: Text
, version :: Text
, dependencies :: [Text]
} deriving (Show, Eq, Generic, Aeson.FromJSON, Aeson.ToJSON)
type PackageSet = Map.Map Text PackageInfo
cloneShallow
:: Text
-- ^ repo
-> Text
-- ^ branch/tag
-> Turtle.FilePath
-- ^ target directory
-> IO ExitCode
cloneShallow from ref into =
proc "git"
[ "clone"
, "-q"
, "-c", "advice.detachedHead=false"
, "--depth", "1"
, "-b", ref
, from
, pathToTextUnsafe into
] empty .||. exit (ExitFailure 1)
getPackageSet :: PackageConfig -> IO ()
getPackageSet PackageConfig{ source, set } = do
let pkgDir = ".psc-package" </> fromText set </> ".set"
exists <- testdir pkgDir
unless exists . void $ cloneShallow source set pkgDir
readPackageSet :: PackageConfig -> IO PackageSet
readPackageSet PackageConfig{ set } = do
let dbFile = ".psc-package" </> fromText set </> ".set" </> "packages.json"
exists <- testfile dbFile
unless exists $ do
echo "packages.json does not exist"
exit (ExitFailure 1)
mdb <- Aeson.decodeStrict . encodeUtf8 <$> readTextFile dbFile
case mdb of
Nothing -> do
echo "Unable to parse packages.json"
exit (ExitFailure 1)
Just db -> return db
installOrUpdate :: PackageConfig -> Text -> PackageInfo -> IO ()
installOrUpdate PackageConfig{ set } pkgName PackageInfo{ repo, version } = do
let pkgDir = ".psc-package" </> fromText set </> fromText pkgName </> fromText version
exists <- testdir pkgDir
unless exists . void $ cloneShallow repo version pkgDir
getTransitiveDeps :: PackageSet -> [Text] -> IO [(Text, PackageInfo)]
getTransitiveDeps db depends = do
pkgs <- for depends $ \pkg ->
case Map.lookup pkg db of
Nothing -> do
echo ("Package " <> pkg <> " does not exist in package set")
exit (ExitFailure 1)
Just PackageInfo{ dependencies } -> return (pkg : dependencies)
let unique = Set.toList (foldMap Set.fromList pkgs)
return (mapMaybe (\name -> fmap (name, ) (Map.lookup name db)) unique)
updateImpl :: PackageConfig -> IO ()
updateImpl config@PackageConfig{ depends } = do
getPackageSet config
db <- readPackageSet config
trans <- getTransitiveDeps db depends
echo ("Updating " <> pack (show (length trans)) <> " packages...")
for_ trans $ \(pkgName, pkg) -> do
echo ("Updating " <> pkgName)
installOrUpdate config pkgName pkg
initialize :: IO ()
initialize = do
exists <- testfile "psc-package.json"
when exists $ do
echo "psc-package.json already exists"
exit (ExitFailure 1)
echo "Initializing new project in current directory"
pkgName <- pathToTextUnsafe . Path.filename <$> pwd
let pkg = defaultPackage pkgName
writePackageFile pkg
updateImpl pkg
update :: IO ()
update = do
pkg <- readPackageFile
updateImpl pkg
echo "Update complete"
install :: String -> IO ()
install pkgName = do
pkg <- readPackageFile
let pkg' = pkg { depends = nub (pack pkgName : depends pkg) }
updateImpl pkg'
writePackageFile pkg'
echo "psc-package.json file was updated"
listDependencies :: IO ()
listDependencies = do
pkg@PackageConfig{ depends } <- readPackageFile
db <- readPackageSet pkg
trans <- getTransitiveDeps db depends
traverse_ (echo . fst) trans
getSourcePaths :: PackageConfig -> PackageSet -> [Text] -> IO [Turtle.FilePath]
getSourcePaths PackageConfig{..} db pkgNames = do
trans <- getTransitiveDeps db pkgNames
let paths = [ ".psc-package"
</> fromText set
</> fromText pkgName
</> fromText version
</> "src" </> "**" </> "*.purs"
| (pkgName, PackageInfo{ version }) <- trans
]
return paths
listSourcePaths :: IO ()
listSourcePaths = do
pkg@PackageConfig{ depends } <- readPackageFile
db <- readPackageSet pkg
paths <- getSourcePaths pkg db depends
traverse_ (echo . pathToTextUnsafe) paths
exec :: Text -> IO ()
exec exeName = do
pkg@PackageConfig{..} <- readPackageFile
db <- readPackageSet pkg
paths <- getSourcePaths pkg db depends
procs exeName
(map pathToTextUnsafe ("src" </> "**" </> "*.purs" : paths))
empty
main :: IO ()
main = do
IO.hSetEncoding IO.stdout IO.utf8
IO.hSetEncoding IO.stderr IO.utf8
cmd <- Opts.execParser opts
cmd
where
opts = Opts.info (versionInfo <*> Opts.helper <*> commands) infoModList
infoModList = Opts.fullDesc <> headerInfo <> footerInfo
headerInfo = Opts.progDesc "Manage package dependencies"
footerInfo = Opts.footer $ "psc-package " ++ showVersion Paths.version
versionInfo :: Parser (a -> a)
versionInfo = Opts.abortOption (Opts.InfoMsg (showVersion Paths.version)) $
Opts.long "version" <> Opts.help "Show the version number" <> Opts.hidden
commands :: Parser (IO ())
commands = (Opts.subparser . fold)
[ Opts.command "init"
(Opts.info (pure initialize)
(Opts.progDesc "Initialize a new package"))
, Opts.command "update"
(Opts.info (pure update)
(Opts.progDesc "Update dependencies"))
, Opts.command "install"
(Opts.info (install <$> pkg)
(Opts.progDesc "Install the named package"))
, Opts.command "build"
(Opts.info (pure (exec "psc"))
(Opts.progDesc "Build the current package and dependencies"))
, Opts.command "dependencies"
(Opts.info (pure listDependencies)
(Opts.progDesc "List all (transitive) dependencies for the current package"))
, Opts.command "sources"
(Opts.info (pure listSourcePaths)
(Opts.progDesc "List all (active) source paths for dependencies"))
]
where
pkg = Opts.strArgument $
Opts.metavar "PACKAGE"
<> Opts.help "The name of the package to install"