diff --git a/app/Main.hs b/app/Main.hs index 968e369..6f9df3c 100644 --- a/app/Main.hs +++ b/app/Main.hs @@ -1,266 +1,43 @@ -{-# LANGUAGE DeriveAnyClass #-} -{-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE RecordWildCards #-} -{-# LANGUAGE TupleSections #-} module Main where import qualified Control.Foldl as Foldl -import Control.Concurrent.Async (forConcurrently_) -import qualified Data.Aeson as Aeson -import Data.Aeson.Encode.Pretty -import Data.Foldable (fold, for_, traverse_) +import Data.Foldable (fold, traverse_) import qualified Data.Graph as G -import Data.List (maximumBy, nub) import qualified Data.Map as Map -import Data.Maybe (fromMaybe, mapMaybe) -import Data.Ord (comparing) -import qualified Data.Set as Set +import Data.Maybe (mapMaybe) import Data.Text (pack) import qualified Data.Text as T -import Data.Text.Encoding (encodeUtf8) -import qualified Data.Text.Lazy as TL -import qualified Data.Text.Lazy.Builder as TB import qualified Data.Text.Read as TR import Data.Traversable (for) -import Data.Version (Version(..), parseVersion, showVersion) +import Data.Version (showVersion) import qualified Filesystem.Path.CurrentOS as Path -import GHC.Generics (Generic) import qualified Options.Applicative as Opts import qualified Paths_psc_package as Paths import System.Environment (getArgs) import qualified System.IO as IO import qualified System.Process as Process -import qualified Text.ParserCombinators.ReadP as Read import Turtle hiding (echo, fold, s, x) import qualified Turtle -import Types (PackageName, mkPackageName, runPackageName, untitledPackageName, preludePackageName) + +import Language.PureScript.Package.Types.PackageConfig (PackageConfig(..), depends, readPackageFile) +import Language.PureScript.Package.Types.PackageInfo (PackageInfo(..), repo, version, dependencies) +import Language.PureScript.Package.Types.PackageName (PackageName, runPackageName) +import Language.PureScript.Package.Types.PackageSet (readPackageSet, writePackageSet, getTransitiveDeps) +import Language.PureScript.Package.Initialize (initialize) +import Language.PureScript.Package.Install (install) +import Language.PureScript.Package.Path (pathToTextUnsafe) +import Language.PureScript.Package.Paths (getPaths) +import Language.PureScript.Package.Git (listRemoteTags) +import Language.PureScript.Package.Uninstall (uninstall) +import Language.PureScript.Package.Update (update, updateImpl) +import Language.PureScript.Package.Verify (verifyPackageSet) echoT :: Text -> IO () echoT = Turtle.printf (Turtle.s % "\n") -exitWithErr :: Text -> IO a -exitWithErr errText = errT errText >> exit (ExitFailure 1) - where errT = traverse Turtle.err . textToLines - -packageFile :: Path.FilePath -packageFile = "psc-package.json" - -data PackageConfig = PackageConfig - { name :: PackageName - , depends :: [PackageName] - , set :: Text - , source :: Text - } deriving (Show, Generic, Aeson.FromJSON, Aeson.ToJSON) - -pathToTextUnsafe :: Turtle.FilePath -> Text -pathToTextUnsafe = either (error "Path.toText failed") id . Path.toText - -readPackageFile :: IO PackageConfig -readPackageFile = do - exists <- testfile packageFile - unless exists $ exitWithErr "psc-package.json does not exist. Maybe you need to run psc-package init?" - mpkg <- Aeson.decodeStrict . encodeUtf8 <$> readTextFile packageFile - case mpkg of - Nothing -> exitWithErr "Unable to parse psc-package.json" - Just pkg -> return pkg - -packageConfigToJSON :: PackageConfig -> Text -packageConfigToJSON = - TL.toStrict - . TB.toLazyText - . encodePrettyToTextBuilder' config - where - config = defConfig - { confCompare = - keyOrder [ "name" - , "set" - , "source" - , "depends" - ] - } - -packageSetToJSON :: PackageSet -> Text -packageSetToJSON = - TL.toStrict - . TB.toLazyText - . encodePrettyToTextBuilder' config - where - config = defConfig { confCompare = compare } - -writePackageFile :: PackageConfig -> IO () -writePackageFile = - writeTextFile packageFile - . packageConfigToJSON - -data PackageInfo = PackageInfo - { repo :: Text - , version :: Text - , dependencies :: [PackageName] - } deriving (Show, Eq, Generic, Aeson.FromJSON, Aeson.ToJSON) - -type PackageSet = Map.Map PackageName 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) - -listRemoteTags - :: Text - -- ^ repo - -> Turtle.Shell Text -listRemoteTags from = let gitProc = inproc "git" - [ "ls-remote" - , "-q" - , "-t" - , from - ] empty - in lineToText <$> gitProc - -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 $ exitWithErr $ format (fp%" does not exist") dbFile - mdb <- Aeson.decodeStrict . encodeUtf8 <$> readTextFile dbFile - case mdb of - Nothing -> exitWithErr "Unable to parse packages.json" - Just db -> return db - -writePackageSet :: PackageConfig -> PackageSet -> IO () -writePackageSet PackageConfig{ set } = - let dbFile = ".psc-package" fromText set ".set" "packages.json" - in writeTextFile dbFile . packageSetToJSON - -installOrUpdate :: Text -> PackageName -> PackageInfo -> IO Turtle.FilePath -installOrUpdate set pkgName PackageInfo{ repo, version } = do - let pkgDir = ".psc-package" fromText set fromText (runPackageName pkgName) fromText version - exists <- testdir pkgDir - unless exists . void $ do - echoT ("Updating " <> runPackageName pkgName) - cloneShallow repo version pkgDir - pure pkgDir - -getTransitiveDeps :: PackageSet -> [PackageName] -> IO [(PackageName, PackageInfo)] -getTransitiveDeps db deps = - Map.toList . fold <$> traverse (go Set.empty) deps - where - go seen pkg - | pkg `Set.member` seen = - exitWithErr ("Cycle in package dependencies at package " <> runPackageName pkg) - | otherwise = - case Map.lookup pkg db of - Nothing -> - exitWithErr ("Package " <> runPackageName pkg <> " does not exist in package set") - Just info@PackageInfo{ dependencies } -> do - m <- fold <$> traverse (go (Set.insert pkg seen)) dependencies - return (Map.insert pkg info m) - -updateImpl :: PackageConfig -> IO () -updateImpl config@PackageConfig{ depends } = do - getPackageSet config - db <- readPackageSet config - trans <- getTransitiveDeps db depends - echoT ("Updating " <> pack (show (length trans)) <> " packages...") - forConcurrently_ trans . uncurry $ installOrUpdate (set config) - -getPureScriptVersion :: IO Version -getPureScriptVersion = do - let pursProc = inproc "purs" [ "--version" ] empty - outputLines <- Turtle.fold (fmap lineToText pursProc) Foldl.list - case outputLines of - [onlyLine] - | results@(_ : _) <- Read.readP_to_S parseVersion (T.unpack onlyLine) -> - pure (fst (maximumBy (comparing (length . versionBranch . fst)) results)) - | otherwise -> exitWithErr "Unable to parse output of purs --version" - _ -> exitWithErr "Unexpected output from purs --version" - -initialize :: Maybe (Text, Maybe Text) -> IO () -initialize setAndSource = do - exists <- testfile "psc-package.json" - when exists $ exitWithErr "psc-package.json already exists" - echoT "Initializing new project in current directory" - pkgName <- packageNameFromPWD . pathToTextUnsafe . Path.filename <$> pwd - pkg <- case setAndSource of - Nothing -> do - pursVersion <- getPureScriptVersion - echoT ("Using the default package set for PureScript compiler version " <> - fromString (showVersion pursVersion)) - echoT "(Use --source / --set to override this behavior)" - pure PackageConfig { name = pkgName - , depends = [ preludePackageName ] - , source = "https://github.com/purescript/package-sets.git" - , set = "psc-" <> pack (showVersion pursVersion) - } - Just (set, source) -> - pure PackageConfig { name = pkgName - , depends = [ preludePackageName ] - , source = fromMaybe "https://github.com/purescript/package-sets.git" source - , set - } - - writePackageFile pkg - updateImpl pkg - where - packageNameFromPWD = - either (const untitledPackageName) id . mkPackageName - -update :: IO () -update = do - pkg <- readPackageFile - updateImpl pkg - echoT "Update complete" - -install :: String -> IO () -install pkgName' = do - pkg <- readPackageFile - pkgName <- packageNameFromString pkgName' - let pkg' = pkg { depends = nub (pkgName : depends pkg) } - updateAndWritePackageFile pkg' - -uninstall :: String -> IO () -uninstall pkgName' = do - pkg <- readPackageFile - pkgName <- packageNameFromString pkgName' - let pkg' = pkg { depends = filter (/= pkgName) $ depends pkg } - updateAndWritePackageFile pkg' - -updateAndWritePackageFile :: PackageConfig -> IO () -updateAndWritePackageFile pkg = do - updateImpl pkg - writePackageFile pkg - echoT "psc-package.json file was updated" - -packageNameFromString :: String -> IO PackageName -packageNameFromString str = - case mkPackageName (pack str) of - Right pkgName -> - pure pkgName - Left _ -> exitWithErr $ "Invalid package name: " <> pack (show str) - listDependencies :: IO () listDependencies = do pkg@PackageConfig{ depends } <- readPackageFile @@ -288,24 +65,6 @@ listPackages sorted = do vs = G.topSort (G.transposeG gr) fromNode (pkg, name, _) = (name, pkg) -getSourcePaths :: PackageConfig -> PackageSet -> [PackageName] -> IO [Turtle.FilePath] -getSourcePaths PackageConfig{..} db pkgNames = do - trans <- getTransitiveDeps db pkgNames - let paths = [ ".psc-package" - fromText set - fromText (runPackageName pkgName) - fromText version - "src" "**" "*.purs" - | (pkgName, PackageInfo{ version }) <- trans - ] - return paths - -getPaths :: IO [Turtle.FilePath] -getPaths = do - pkg@PackageConfig{..} <- readPackageFile - db <- readPackageSet pkg - getSourcePaths pkg db depends - listSourcePaths :: IO () listSourcePaths = do paths <- getPaths @@ -412,24 +171,6 @@ checkForUpdates applyMinorUpdates applyMajorUpdates = do isMinorReleaseFrom (x : xs) (y : ys) = y == x && ys > xs isMinorReleaseFrom _ _ = False -verifyPackageSet :: IO () -verifyPackageSet = do - pkg <- readPackageFile - db <- readPackageSet pkg - - echoT ("Verifying " <> pack (show (Map.size db)) <> " packages.") - echoT "Warning: this could take some time!" - - let installOrUpdate' (name, pkgInfo) = (name, ) <$> installOrUpdate (set pkg) name pkgInfo - paths <- Map.fromList <$> traverse installOrUpdate' (Map.toList db) - - for_ (Map.toList db) $ \(name, _) -> do - let dirFor pkgName = fromMaybe (error ("verifyPackageSet: no directory for " <> show pkgName)) (Map.lookup pkgName paths) - echoT ("Verifying package " <> runPackageName name) - dependencies <- map fst <$> getTransitiveDeps db [name] - let srcGlobs = map (pathToTextUnsafe . ( ("src" "**" "*.purs")) . dirFor) dependencies - procs "purs" ("compile" : srcGlobs) empty - main :: IO () main = do IO.hSetEncoding IO.stdout IO.utf8 @@ -462,10 +203,10 @@ main = do (Opts.info (pure update) (Opts.progDesc "Update dependencies")) , Opts.command "uninstall" - (Opts.info (uninstall <$> pkg Opts.<**> Opts.helper) + (Opts.info (uninstall . T.pack <$> pkg Opts.<**> Opts.helper) (Opts.progDesc "Uninstall the named package")) , Opts.command "install" - (Opts.info (install <$> pkg Opts.<**> Opts.helper) + (Opts.info (install . T.pack <$> pkg Opts.<**> Opts.helper) (Opts.progDesc "Install the named package")) , Opts.command "build" (Opts.info (exec ["purs", "compile"] diff --git a/psc-package.cabal b/psc-package.cabal index e3533bb..ceac3a5 100644 --- a/psc-package.cabal +++ b/psc-package.cabal @@ -1,5 +1,5 @@ name: psc-package -version: 0.2.0 +version: 0.3.0 synopsis: An experimental package manager for PureScript description: homepage: https://github.com/purescript/psc-package @@ -13,11 +13,36 @@ build-type: Simple extra-source-files: README.md cabal-version: >=1.10 +library + hs-source-dirs: src + ghc-options: -Wall -fwarn-tabs -fwarn-incomplete-uni-patterns -fwarn-incomplete-record-updates -fwarn-identities + exposed-modules: Language.PureScript.Package.Types.PackageConfig, + Language.PureScript.Package.Types.PackageInfo, + Language.PureScript.Package.Types.PackageName, + Language.PureScript.Package.Types.PackageSet, + Language.PureScript.Package.Echo, + Language.PureScript.Package.Git, + Language.PureScript.Package.Initialize, + Language.PureScript.Package.Install, + Language.PureScript.Package.Path, + Language.PureScript.Package.Paths, + Language.PureScript.Package.Uninstall, + Language.PureScript.Package.Update, + Language.PureScript.Package.Verify + build-depends: base >= 4.7 && < 5, + aeson -any, + aeson-pretty -any, + async -any, + containers, + foldl -any, + text -any, + transformers -any, + turtle == 1.3.* + default-language: Haskell2010 + executable psc-package - build-depends: base >=4 && <5, - aeson -any, - aeson-pretty -any, - async -any, + build-depends: psc-package, + base >=4 && <5, bytestring -any, containers -any, foldl -any, @@ -28,8 +53,7 @@ executable psc-package turtle ==1.3.* main-is: Main.hs other-modules: Paths_psc_package - Types buildable: True hs-source-dirs: app - ghc-options: -Wall -O2 -threaded -rtsopts -with-rtsopts=-N + ghc-options: -Wall -fwarn-tabs -fwarn-incomplete-uni-patterns -fwarn-incomplete-record-updates -O2 -threaded -rtsopts -with-rtsopts=-N default-language: Haskell2010 diff --git a/src/Language/PureScript/Package/Echo.hs b/src/Language/PureScript/Package/Echo.hs new file mode 100644 index 0000000..d32718c --- /dev/null +++ b/src/Language/PureScript/Package/Echo.hs @@ -0,0 +1,45 @@ +-- | Tools for optionally `echo`ing 'Text' to stdout. +-- +-- Use 'runSilentT' to silence 'echo'ed output in any 'MonadEcho'. + +module Language.PureScript.Package.Echo ( + MonadEcho(..) +, SilentT(..) +) where + +import Control.Monad.IO.Class (MonadIO, liftIO) +import Control.Monad.Trans.Class (MonadTrans, lift) +import Data.Text (Text) +import Data.Text.IO (putStrLn) +import Prelude hiding (putStrLn) + +class Applicative m => MonadEcho m where + echo :: Text -> m () +instance MonadEcho IO where + echo = putStrLn + +-- | A `MonadEcho` that silences `echo`. +newtype SilentT m a = SilentT {runSilentT :: m a } + +instance Functor m => Functor (SilentT m) where + fmap f = SilentT . fmap f . runSilentT + {-# INLINE fmap #-} + +instance Applicative m => Applicative (SilentT m) where + pure = SilentT . pure + {-# INLINE pure #-} + f <*> a = SilentT (runSilentT f <*> runSilentT a) + {-# INLINE (<*>) #-} + +instance Monad m => Monad (SilentT m) where + return = SilentT . return + SilentT ma >>= f = SilentT $ ma >>= runSilentT . f + +instance MonadIO m => MonadIO (SilentT m) where + liftIO = lift . liftIO + +instance Applicative m => MonadEcho (SilentT m) where + echo = const . SilentT $ pure () + +instance MonadTrans SilentT where + lift = SilentT diff --git a/src/Language/PureScript/Package/Git.hs b/src/Language/PureScript/Package/Git.hs new file mode 100644 index 0000000..1e22acb --- /dev/null +++ b/src/Language/PureScript/Package/Git.hs @@ -0,0 +1,45 @@ +{-# LANGUAGE OverloadedStrings #-} + +-- | Functions for executing git. + +module Language.PureScript.Package.Git ( + cloneShallow +, listRemoteTags +) where + +import Turtle +import Prelude hiding (FilePath) + +import Language.PureScript.Package.Path (pathToTextUnsafe) + +cloneShallow + :: MonadIO m + => Text + -- ^ repo + -> Text + -- ^ branch/tag + -> Turtle.FilePath + -- ^ target directory + -> m ExitCode +cloneShallow from ref into = + proc "git" + [ "clone" + , "-q" + , "-c", "advice.detachedHead=false" + , "--depth", "1" + , "-b", ref + , from + , pathToTextUnsafe into + ] empty .||. exit (ExitFailure 1) + +listRemoteTags + :: Text + -- ^ repo + -> Turtle.Shell Text +listRemoteTags from = let gitProc = inproc "git" + [ "ls-remote" + , "-q" + , "-t" + , from + ] empty + in lineToText <$> gitProc diff --git a/src/Language/PureScript/Package/Initialize.hs b/src/Language/PureScript/Package/Initialize.hs new file mode 100644 index 0000000..046c6f0 --- /dev/null +++ b/src/Language/PureScript/Package/Initialize.hs @@ -0,0 +1,61 @@ +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE OverloadedStrings #-} + +-- | Initialize a new package, generating its psc-package.json file. + +module Language.PureScript.Package.Initialize (initialize) where + +import qualified Control.Foldl as Foldl +import Data.List (maximumBy) +import Data.Maybe (fromMaybe) +import Data.Ord (comparing) +import qualified Data.Text as T +import Data.Version (Version(..), parseVersion, showVersion) +import Text.ParserCombinators.ReadP (readP_to_S) +import Turtle hiding (echo) +import Prelude hiding (FilePath) + +import Language.PureScript.Package.Echo (MonadEcho(..), echo) +import Language.PureScript.Package.Types.PackageConfig (PackageConfig(..), name, depends, set, source, writePackageFile) +import Language.PureScript.Package.Types.PackageName (untitledPackageName, mkPackageName, preludePackageName) +import Language.PureScript.Package.Path (pathToTextUnsafe) +import Language.PureScript.Package.Update (updateImpl) + +initialize :: (MonadIO m, MonadEcho m) => Maybe (Text, Maybe Text) -> m () +initialize setAndSource = do + exists <- testfile "psc-package.json" + when exists $ die "psc-package.json already exists" + echo "Initializing new project in current directory" + pkgName <- packageNameFromPWD . pathToTextUnsafe . filename <$> pwd + pkg <- case setAndSource of + Nothing -> do + pursVersion <- getPureScriptVersion + echo $ "Using the default package set for PureScript compiler version " <> + fromString (showVersion pursVersion) + echo "(Use --source / --set to override this behavior)" + pure PackageConfig { name = pkgName + , depends = [ preludePackageName ] + , source = defaultSource + , set = "psc-" <> T.pack (showVersion pursVersion) + } + Just (set, source) -> + pure PackageConfig { name = pkgName + , depends = [ preludePackageName ] + , source = fromMaybe defaultSource source + , set + } + + writePackageFile pkg + updateImpl pkg + where + defaultSource = "https://github.com/purescript/package-sets.git" + packageNameFromPWD = either (const untitledPackageName) id . mkPackageName + getPureScriptVersion = do + let pursProc = inproc "purs" [ "--version" ] empty + outputLines <- Turtle.fold (fmap lineToText pursProc) Foldl.list + case outputLines of + [onlyLine] + | results@(_ : _) <- readP_to_S parseVersion (T.unpack onlyLine) -> + pure (fst (maximumBy (comparing (length . versionBranch . fst)) results)) + | otherwise -> die "Unable to parse output of purs --version" + _ -> die "Unexpected output from purs --version" diff --git a/src/Language/PureScript/Package/Install.hs b/src/Language/PureScript/Package/Install.hs new file mode 100644 index 0000000..676306c --- /dev/null +++ b/src/Language/PureScript/Package/Install.hs @@ -0,0 +1,23 @@ +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE NamedFieldPuns #-} + +-- | Install a new package and update the package file. + +module Language.PureScript.Package.Install (install) where + +import Control.Monad.IO.Class (MonadIO) +import Data.List (nub) +import Data.Text (Text) +import Prelude hiding (FilePath) + +import Language.PureScript.Package.Echo (MonadEcho(..)) +import Language.PureScript.Package.Types.PackageConfig (readPackageFile, depends) +import Language.PureScript.Package.Types.PackageName (fromText) +import Language.PureScript.Package.Update (updateAndWritePackageFile) + +install :: (MonadIO m, MonadEcho m) => Text -> m () +install pkgName' = do + pkg <- readPackageFile + pkgName <- fromText pkgName' + let pkg' = pkg { depends = nub (pkgName : depends pkg) } -- TODO: ordnub + updateAndWritePackageFile pkg' diff --git a/src/Language/PureScript/Package/Path.hs b/src/Language/PureScript/Package/Path.hs new file mode 100644 index 0000000..4027f84 --- /dev/null +++ b/src/Language/PureScript/Package/Path.hs @@ -0,0 +1,9 @@ +-- | Convert 'FilePath's to 'Text' + +module Language.PureScript.Package.Path (pathToTextUnsafe) where + +import Turtle (FilePath, Text, toText) +import Prelude (either, error, id, (.)) + +pathToTextUnsafe :: FilePath -> Text +pathToTextUnsafe = either (error "FilePath.toText failed") id . toText diff --git a/src/Language/PureScript/Package/Paths.hs b/src/Language/PureScript/Package/Paths.hs new file mode 100644 index 0000000..47f82ce --- /dev/null +++ b/src/Language/PureScript/Package/Paths.hs @@ -0,0 +1,34 @@ +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE RecordWildCards #-} + +-- | Get the source paths for a package. + +module Language.PureScript.Package.Paths (getPaths) where + +import Turtle +import Prelude hiding (FilePath) + +import Language.PureScript.Package.Types.PackageConfig (PackageConfig(..), readPackageFile) +import Language.PureScript.Package.Types.PackageInfo (PackageInfo(..)) +import Language.PureScript.Package.Types.PackageName (PackageName, runPackageName) +import Language.PureScript.Package.Types.PackageSet (PackageSet, readPackageSet, getTransitiveDeps) + +getPaths :: MonadIO m => m [FilePath] +getPaths = do + pkg@PackageConfig{..} <- readPackageFile + db <- readPackageSet pkg + getSourcePaths pkg db depends + where + getSourcePaths :: MonadIO m => PackageConfig -> PackageSet -> [PackageName] -> m [FilePath] + getSourcePaths PackageConfig{..} db pkgNames = do + trans <- getTransitiveDeps db pkgNames + let paths = [ ".psc-package" + fromText set + fromText (runPackageName pkgName) + fromText version + "src" "**" "*.purs" + | (pkgName, PackageInfo{ version }) <- trans + ] + return paths + diff --git a/src/Language/PureScript/Package/Types/PackageConfig.hs b/src/Language/PureScript/Package/Types/PackageConfig.hs new file mode 100644 index 0000000..f43d528 --- /dev/null +++ b/src/Language/PureScript/Package/Types/PackageConfig.hs @@ -0,0 +1,59 @@ +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE DeriveAnyClass #-} +{-# LANGUAGE DeriveGeneric #-} + +module Language.PureScript.Package.Types.PackageConfig ( + PackageConfig(..) +, readPackageFile +, writePackageFile +) where + +import Data.Aeson (FromJSON, ToJSON) +import qualified Data.Aeson as Aeson +import qualified Data.Aeson.Encode.Pretty as AesonEncode +import Data.Text (Text) +import Data.Text.Encoding (encodeUtf8) +import qualified Data.Text.Lazy as TL +import qualified Data.Text.Lazy.Builder as TB +import GHC.Generics (Generic) +import Turtle +import Prelude hiding (FilePath) + +import Language.PureScript.Package.Types.PackageName (PackageName) + +data PackageConfig = PackageConfig + { name :: PackageName + , set :: Text + , source :: Text + , depends :: [PackageName] + } deriving (Show, Generic, FromJSON, ToJSON) + +packageConfigToJSON :: PackageConfig -> Text +packageConfigToJSON = + TL.toStrict + . TB.toLazyText + . AesonEncode.encodePrettyToTextBuilder' config + where + config = AesonEncode.defConfig + { AesonEncode.confCompare = + AesonEncode.keyOrder [ "name" + , "set" + , "source" + , "depends" + ] + } + +packageFile :: FilePath +packageFile = "psc-package.json" + +readPackageFile :: MonadIO m => m PackageConfig +readPackageFile = do + exists <- testfile packageFile + unless exists $ die "psc-package.json does not exist. Maybe you need to run psc-package init?" + mpkg <- liftIO $ Aeson.decodeStrict . encodeUtf8 <$> readTextFile packageFile + case mpkg of + Nothing -> die "Unable to parse psc-package.json" + Just pkg -> return pkg + +writePackageFile :: MonadIO m => PackageConfig -> m () +writePackageFile = liftIO . writeTextFile packageFile . packageConfigToJSON diff --git a/src/Language/PureScript/Package/Types/PackageInfo.hs b/src/Language/PureScript/Package/Types/PackageInfo.hs new file mode 100644 index 0000000..bb5486c --- /dev/null +++ b/src/Language/PureScript/Package/Types/PackageInfo.hs @@ -0,0 +1,17 @@ +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE DeriveAnyClass #-} +{-# LANGUAGE DeriveGeneric #-} + +module Language.PureScript.Package.Types.PackageInfo (PackageInfo(..)) where + +import Data.Aeson (FromJSON, ToJSON) +import Data.Text (Text) +import GHC.Generics (Generic) + +import Language.PureScript.Package.Types.PackageName (PackageName) + +data PackageInfo = PackageInfo + { repo :: Text + , version :: Text + , dependencies :: [PackageName] + } deriving (Show, Eq, Generic, FromJSON, ToJSON) diff --git a/app/Types.hs b/src/Language/PureScript/Package/Types/PackageName.hs similarity index 88% rename from app/Types.hs rename to src/Language/PureScript/Package/Types/PackageName.hs index 2d5e4e5..14b450d 100644 --- a/app/Types.hs +++ b/src/Language/PureScript/Package/Types/PackageName.hs @@ -1,9 +1,11 @@ {-# LANGUAGE OverloadedStrings #-} -module Types +module Language.PureScript.Package.Types.PackageName ( PackageName , mkPackageName + , fromText , runPackageName + , packageNameFromString , preludePackageName , untitledPackageName ) where @@ -15,6 +17,7 @@ import Data.Char (isAscii, isLower, isDigit) import Data.Monoid ((<>)) import Data.Text (Text) import qualified Data.Text as T +import Turtle (die) newtype PackageName = PackageName Text @@ -78,6 +81,12 @@ mkPackageName = fmap PackageName . validateAll validators runPackageName :: PackageName -> Text runPackageName (PackageName t) = t +packageNameFromString :: String -> IO PackageName +packageNameFromString str = + case mkPackageName (T.pack str) of + Right pkgName -> pure pkgName + Left _ -> die $ "Invalid package name: " <> T.pack (show str) + preludePackageName :: PackageName preludePackageName = PackageName "prelude" diff --git a/src/Language/PureScript/Package/Types/PackageSet.hs b/src/Language/PureScript/Package/Types/PackageSet.hs new file mode 100644 index 0000000..e7f6943 --- /dev/null +++ b/src/Language/PureScript/Package/Types/PackageSet.hs @@ -0,0 +1,79 @@ +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE NamedFieldPuns #-} + +module Language.PureScript.Package.Types.PackageSet ( + PackageSet +, packageSetToJSON +, getPackageSet +, readPackageSet +, writePackageSet +, getTransitiveDeps +) where + +import qualified Data.Aeson as Aeson +import qualified Data.Aeson.Encode.Pretty as AesonEncode +import Data.Foldable (fold) +import Data.Map (Map) +import qualified Data.Map as Map +import qualified Data.Set as Set +import Data.Text.Encoding (encodeUtf8) +import qualified Data.Text.Lazy as TL +import qualified Data.Text.Lazy.Builder as TB +import Turtle hiding (fold) +import Prelude hiding (FilePath) + +import Language.PureScript.Package.Types.PackageConfig (PackageConfig(..)) +import Language.PureScript.Package.Types.PackageInfo (PackageInfo(..)) +import Language.PureScript.Package.Types.PackageName (PackageName, runPackageName) +import Language.PureScript.Package.Git (cloneShallow) + +type PackageSet = Map PackageName PackageInfo + +packageSetToJSON :: PackageSet -> Text +packageSetToJSON = + TL.toStrict + . TB.toLazyText + . AesonEncode.encodePrettyToTextBuilder' config + where + config = AesonEncode.defConfig { AesonEncode.confCompare = compare } + +getPackageSet :: MonadIO m => PackageConfig -> m () +getPackageSet PackageConfig{ source, set } = do + let pkgDir = ".psc-package" fromText set ".set" + exists <- testdir pkgDir + unless exists . void $ cloneShallow source set pkgDir + +readPackageSet :: MonadIO m => PackageConfig -> m PackageSet +readPackageSet PackageConfig{ set } = do + let dbFile = ".psc-package" fromText set ".set" "packages.json" + exists <- testfile dbFile + unless exists $ die $ format (fp%" does not exist") dbFile + mdb <- liftIO $ Aeson.decodeStrict . encodeUtf8 <$> readTextFile dbFile + case mdb of + Nothing -> die "Unable to parse packages.json" + Just db -> return db + +writePackageSet :: PackageConfig -> PackageSet -> IO () +writePackageSet PackageConfig{ set } = + let dbFile = ".psc-package" fromText set ".set" "packages.json" + in writeTextFile dbFile . packageSetToJSON + +getTransitiveDeps + :: MonadIO m + => PackageSet + -> [PackageName] + -> m [(PackageName, PackageInfo)] +getTransitiveDeps db deps = + Map.toList . fold <$> traverse (go Set.empty) deps + where + go seen pkg + | pkg `Set.member` seen = + die $ "Cycle in package dependencies at package " <> runPackageName pkg + | otherwise = + case Map.lookup pkg db of + Nothing -> + die $ "Package " <> runPackageName pkg <> " does not exist in package set" + Just info@PackageInfo{ dependencies } -> do + m <- fold <$> traverse (go (Set.insert pkg seen)) dependencies + return (Map.insert pkg info m) + diff --git a/src/Language/PureScript/Package/Uninstall.hs b/src/Language/PureScript/Package/Uninstall.hs new file mode 100644 index 0000000..ccfd36f --- /dev/null +++ b/src/Language/PureScript/Package/Uninstall.hs @@ -0,0 +1,18 @@ +module Language.PureScript.Package.Uninstall (uninstall) where + +-- | Remove a package from the package file. + +import Control.Monad.IO.Class (MonadIO) +import Data.Text (Text) + +import Language.PureScript.Package.Echo (MonadEcho(..)) +import Language.PureScript.Package.Types.PackageConfig (readPackageFile, depends) +import Language.PureScript.Package.Types.PackageName (fromText) +import Language.PureScript.Package.Update (updateAndWritePackageFile) + +uninstall :: (MonadIO m, MonadEcho m) => Text -> m () +uninstall pkgName' = do + pkg <- readPackageFile + pkgName <- fromText pkgName' + let pkg' = pkg { depends = filter (/= pkgName) $ depends pkg } + updateAndWritePackageFile pkg' diff --git a/src/Language/PureScript/Package/Update.hs b/src/Language/PureScript/Package/Update.hs new file mode 100644 index 0000000..f9463ce --- /dev/null +++ b/src/Language/PureScript/Package/Update.hs @@ -0,0 +1,59 @@ +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE NamedFieldPuns #-} + +-- | Update (obtain) dependencies/packages. + +module Language.PureScript.Package.Update ( + update +, updateImpl +, updateAndWritePackageFile +, installOrUpdate +) where + +import Control.Concurrent.Async (forConcurrently_) +import qualified Data.Text as T +import Turtle hiding (echo) +import Prelude hiding (FilePath) + +import Language.PureScript.Package.Echo (MonadEcho(..)) +import Language.PureScript.Package.Git (cloneShallow) +import Language.PureScript.Package.Types.PackageConfig (PackageConfig(..), readPackageFile, writePackageFile) +import Language.PureScript.Package.Types.PackageInfo (PackageInfo(..)) +import Language.PureScript.Package.Types.PackageName (PackageName, runPackageName) +import Language.PureScript.Package.Types.PackageSet (readPackageSet, getTransitiveDeps) + +update :: (MonadIO m, MonadEcho m) => m () +update = do + pkg <- readPackageFile + updateImpl pkg + echo "Update complete" + +updateImpl :: (MonadIO m, MonadEcho m) => PackageConfig -> m () +updateImpl config@PackageConfig{ depends } = do + getPackageSet config + db <- readPackageSet config + trans <- getTransitiveDeps db depends + echo $ "Updating " <> T.pack (show (length trans)) <> " packages..." + liftIO . forConcurrently_ trans . uncurry $ installOrUpdate (set config) + + where + getPackageSet :: MonadIO m => PackageConfig -> m () + getPackageSet PackageConfig{ source, set } = do + let pkgDir = ".psc-package" fromText set ".set" + exists <- testdir pkgDir + unless exists . void $ cloneShallow source set pkgDir + +updateAndWritePackageFile :: (MonadIO m, MonadEcho m) => PackageConfig -> m () +updateAndWritePackageFile pkg = do + updateImpl pkg + writePackageFile pkg + echo "psc-package.json file was updated" + +installOrUpdate :: (MonadIO m, MonadEcho m) => Text -> PackageName -> PackageInfo -> m FilePath +installOrUpdate set pkgName PackageInfo{ repo, version } = do + let pkgDir = ".psc-package" fromText set fromText (runPackageName pkgName) fromText version + exists <- testdir pkgDir + unless exists . void $ do + echo $ "Updating " <> runPackageName pkgName + cloneShallow repo version pkgDir + pure pkgDir diff --git a/src/Language/PureScript/Package/Verify.hs b/src/Language/PureScript/Package/Verify.hs new file mode 100644 index 0000000..a9ac926 --- /dev/null +++ b/src/Language/PureScript/Package/Verify.hs @@ -0,0 +1,40 @@ +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE TupleSections #-} + +-- | Check that a package set can compile. + +module Language.PureScript.Package.Verify (verifyPackageSet) where + +import Data.Foldable (for_) +import qualified Data.Map as Map +import Data.Maybe (fromMaybe) +import qualified Data.Text as T +import Turtle hiding (echo) +import Prelude hiding (FilePath) + +import Language.PureScript.Package.Echo (MonadEcho(..)) +import Language.PureScript.Package.Path (pathToTextUnsafe) +import Language.PureScript.Package.Types.PackageConfig (readPackageFile, set) +import Language.PureScript.Package.Types.PackageName (runPackageName) +import Language.PureScript.Package.Types.PackageSet (readPackageSet, getTransitiveDeps) +import Language.PureScript.Package.Update (installOrUpdate) + +verifyPackageSet :: (MonadIO m, MonadEcho m) => m () +verifyPackageSet = do + pkg <- readPackageFile + db <- readPackageSet pkg + + echo $ + "Verifying " <> T.pack (show (Map.size db)) <> " packages." + echo "Warning: this could take some time!" + + let installOrUpdate' (name, pkgInfo) = (name, ) <$> installOrUpdate (set pkg) name pkgInfo + paths <- Map.fromList <$> traverse installOrUpdate' (Map.toList db) + + for_ (Map.toList db) $ \(name, _) -> do + let dirFor pkgName = fromMaybe (error $ "verifyPackageSet: no directory for " <> show pkgName) (Map.lookup pkgName paths) + echo $ "Verifying package " <> runPackageName name + dependencies <- map fst <$> getTransitiveDeps db [name] + let srcGlobs = map (pathToTextUnsafe . ( ("src" "**" "*.purs")) . dirFor) dependencies + procs "purs" ("compile" : srcGlobs) empty + diff --git a/stack.yaml b/stack.yaml index fc491c7..4c7e5e1 100644 --- a/stack.yaml +++ b/stack.yaml @@ -1,4 +1,4 @@ -resolver: lts-8.15 +resolver: lts-9.0 packages: - '.' extra-deps: []