1
1
mirror of https://github.com/nmattia/niv.git synced 2024-11-07 22:36:53 +03:00
niv/Main.hs

421 lines
13 KiB
Haskell
Raw Normal View History

2019-01-23 23:55:26 +03:00
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
2019-01-24 23:58:22 +03:00
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE ViewPatterns #-}
2019-01-23 23:55:26 +03:00
2019-01-18 01:00:48 +03:00
-- TODO: qualified imports
-- TODO: format code
import Control.Monad
2019-01-27 01:39:38 +03:00
import Control.Monad.State
2019-01-23 23:55:26 +03:00
import Data.Aeson
import Data.Bifunctor
2019-01-24 23:58:22 +03:00
import Data.Char (toUpper)
2019-01-23 23:55:26 +03:00
import Data.Hashable (Hashable)
2019-01-27 01:39:38 +03:00
import Data.Maybe (mapMaybe)
2019-01-18 01:00:48 +03:00
import Data.Semigroup ((<>))
2019-01-27 01:39:38 +03:00
import Data.String
import GHC.Exts (toList)
2019-01-23 23:55:26 +03:00
import Options.Applicative
2019-01-18 01:00:48 +03:00
import System.Directory
import System.FilePath
2019-01-23 23:55:26 +03:00
import qualified Data.ByteString as B
import qualified Data.ByteString.Lazy as L
2019-01-24 23:58:22 +03:00
import qualified Data.HashMap.Strict as HMap
import qualified Data.List.NonEmpty as NE
2019-01-23 23:55:26 +03:00
import qualified Data.Text as T
2019-01-27 01:39:38 +03:00
import qualified GitHub as GH
import qualified GitHub.Data.Name as GH
2019-01-18 01:00:48 +03:00
fileFetchNix :: FilePath
fileFetchNix = "nix" </> "fetch.nix"
2019-01-23 23:55:26 +03:00
-- TODO: file "nix/default.nix"
2019-01-18 01:00:48 +03:00
fileFetchNixContent :: String
fileFetchNixContent = unlines
[
]
fileVersionsJson :: FilePath
fileVersionsJson = "nix" </> "versions.json"
fileVersionsJsonContent :: String
fileVersionsJsonContent = unlines
[
]
2019-01-24 23:58:22 +03:00
newtype VersionsSpec = VersionsSpec
{ unVersionsSpec :: HMap.HashMap PackageName PackageSpec }
deriving newtype (FromJSON, ToJSON)
2019-01-23 23:55:26 +03:00
getVersionsSpec :: IO VersionsSpec
getVersionsSpec = do
putStrLn $ "Reading versions file"
decodeFileStrict fileVersionsJson >>= \case
Just (Object v) ->
fmap (VersionsSpec . mconcat) $
forM (HMap.toList v) $ \(k, v) ->
case v of
Object v' ->
pure $ HMap.singleton (PackageName (T.unpack k)) (PackageSpec v')
_ -> error "baaaaz"
Just _ -> error "foo"
Nothing -> error "Cannot decode versions"
newtype PackageName = PackageName { unPackageName :: String }
2019-01-24 23:58:22 +03:00
deriving newtype (Eq, Hashable, FromJSONKey, ToJSONKey, Show)
parsePackageName :: Parser PackageName
parsePackageName = PackageName <$> argument str (metavar "PACKAGE")
2019-01-23 23:55:26 +03:00
newtype PackageSpec = PackageSpec { unPackageSpec :: Object }
2019-01-24 23:58:22 +03:00
deriving newtype (FromJSON, ToJSON, Show)
2019-01-23 23:55:26 +03:00
2019-01-24 23:58:22 +03:00
parsePackageSpec :: Parser PackageSpec
parsePackageSpec =
(PackageSpec . HMap.fromList . fmap fixupAttributes) <$>
many parseAttribute
where
parseAttribute :: Parser (String, String)
parseAttribute = shortcutAttributes <|>
option (maybeReader parseKeyVal)
( long "attribute" <>
short 'a' <>
metavar "KEY=VAL"
)
2019-01-23 23:55:26 +03:00
2019-01-24 23:58:22 +03:00
-- Parse "key=val" into ("key", "val")
parseKeyVal :: String -> Maybe (String, String)
parseKeyVal str = case span (/= '=') str of
(key, '=':val) -> Just (key, val)
_ -> Nothing
-- Shortcuts for common attributes
shortcutAttributes :: Parser (String, String)
shortcutAttributes = foldr (<|>) empty $ mkShortcutAttribute <$>
[ "branch", "name", "owner", "repo" ]
mkShortcutAttribute :: String -> Parser (String, String)
mkShortcutAttribute attr@(c:_) = (attr,) <$> strOption
( long attr <> short c <> metavar (toUpper <$> attr) )
fixupAttributes :: (String, String) -> (T.Text, Value)
fixupAttributes (k, v) = (T.pack k, String (T.pack v))
parsePackage :: Parser (PackageName, PackageSpec)
parsePackage = (,) <$> parsePackageName <*> parsePackageSpec
2019-01-27 01:39:38 +03:00
-------------------------------------------------------------------------------
-- PackageSpec State helpers
-------------------------------------------------------------------------------
2019-01-23 23:55:26 +03:00
2019-01-27 01:39:38 +03:00
whenNotSet
:: T.Text
-> StateT (PackageName, PackageSpec) IO ()
-> StateT (PackageName, PackageSpec) IO ()
whenNotSet attrName act = getPackageSpecAttr attrName >>= \case
Just _ -> pure ()
Nothing -> act
withPackageSpecAttr
:: T.Text
-> (Value -> StateT (PackageName, PackageSpec) IO ())
-> StateT (PackageName, PackageSpec) IO ()
withPackageSpecAttr attrName act = getPackageSpecAttr attrName >>= \case
Just v -> act v
Nothing -> pure ()
getPackageSpecAttr
:: T.Text
-> StateT (PackageName, PackageSpec) IO (Maybe Value)
getPackageSpecAttr attrName = do
(_, PackageSpec obj) <- get
pure $ HMap.lookup attrName obj
setPackageSpecAttr
:: T.Text -> Value
-> StateT (PackageName, PackageSpec) IO ()
setPackageSpecAttr attrName attrValue = do
(packageName, PackageSpec obj) <- get
let obj' = HMap.insert attrName attrValue obj
put (packageName, PackageSpec obj')
setPackageName
:: String -> StateT (PackageName, PackageSpec) IO ()
setPackageName packageName = do
(_, spec) <- get
put (PackageName packageName, spec)
hasPackageSpecAttrs
:: [String]
-> StateT (PackageName, PackageSpec) IO Bool
hasPackageSpecAttrs attrNames = do
(_, PackageSpec obj) <- get
pure $ all (\k -> HMap.member (T.pack k) obj) attrNames
packageSpecStringValues :: PackageSpec -> [(String, String)]
packageSpecStringValues (PackageSpec m) = mapMaybe toVal (HMap.toList m)
where
toVal :: (T.Text, Value) -> Maybe (String, String)
toVal = \case
(key, String val) -> Just (T.unpack key, T.unpack val)
_ -> Nothing
2019-01-23 23:55:26 +03:00
-------------------------------------------------------------------------------
-- INIT
-------------------------------------------------------------------------------
parseCmdInit :: ParserInfo (IO ())
parseCmdInit = (info (pure cmdInit <**> helper)) fullDesc
2019-01-18 01:00:48 +03:00
cmdInit :: IO ()
cmdInit = do
putStrLn "Creating directory nix (if it doesn't exist)"
createDirectoryIfMissing True "nix"
putStrLn $ "Creating file " <> fileFetchNix <> " (if it doesn't exist)"
fileFetchNixExists <- doesFileExist fileFetchNix
if fileFetchNixExists
then do
putStrLn $ "Not writing " <> fileFetchNix
putStrLn "(file exists)"
else do
putStrLn $ "Writing " <> fileFetchNix
writeFile fileFetchNix fileFetchNixContent
putStrLn $ "Creating file " <> fileVersionsJson <> " (if it doesn't exist)"
fileVersionsJsonExists <- doesFileExist fileVersionsJson
if fileVersionsJsonExists
then do
putStrLn $ "Not writing " <> fileVersionsJson
putStrLn "(file exists)"
else do
putStrLn $ "Writing " <> fileVersionsJson
writeFile fileVersionsJson fileVersionsJsonContent
2019-01-23 23:55:26 +03:00
-------------------------------------------------------------------------------
-- ADD
-------------------------------------------------------------------------------
2019-01-24 23:58:22 +03:00
parseCmdAdd :: ParserInfo (IO ())
2019-01-27 01:39:38 +03:00
parseCmdAdd = (info ((cmdAdd <$> parsePackage) <**> helper)) fullDesc
2019-01-23 23:55:26 +03:00
2019-01-27 01:39:38 +03:00
cmdAdd :: (PackageName, PackageSpec) -> IO ()
cmdAdd package = do
2019-01-23 23:55:26 +03:00
2019-01-27 01:39:38 +03:00
(packageName, packageSpec) <- addCompletePackageSpec package
2019-01-23 23:55:26 +03:00
2019-01-27 01:39:38 +03:00
versionsSpec <- HMap.insert packageName packageSpec . unVersionsSpec <$>
getVersionsSpec
2019-01-23 23:55:26 +03:00
putStrLn $ "Writing new versions file"
2019-01-27 01:39:38 +03:00
print versionsSpec
-- encodeFile fileVersionsJson fileVersionsValue'
2019-01-23 23:55:26 +03:00
2019-01-24 23:58:22 +03:00
addCompletePackageSpec
:: (PackageName, PackageSpec)
-> IO (PackageName, PackageSpec)
2019-01-27 01:39:38 +03:00
addCompletePackageSpec x@(PackageName str, _) = flip execStateT x $ do
-- Figures out the owner and repo
case span (/= '/') str of
(owner@(_:_), '/':repo@(_:_)) -> do
whenNotSet "owner" $
setPackageSpecAttr "owner" (String $ T.pack owner)
whenNotSet "repo" $ do
setPackageSpecAttr "repo" (String $ T.pack repo)
setPackageName repo
_ -> pure ()
-- In case we have @owner@ and @repo@, pull some data from GitHub
(,) <$> getPackageSpecAttr "owner" <*> getPackageSpecAttr "repo" >>= \case
(Just (String owner), Just (String repo)) -> do
liftIO (GH.executeRequest' $ GH.repositoryR (GH.N owner) (GH.N repo))
>>= \case
Right ghRepo -> do
-- Description
whenNotSet "description" $ case GH.repoDescription ghRepo of
Just descr -> setPackageSpecAttr "description" (String descr)
Nothing -> pure ()
-- Branch and rev
whenNotSet "branch" $ case GH.repoDefaultBranch ghRepo of
Just branch -> do
setPackageSpecAttr "branch" (String branch)
liftIO (GH.executeRequest' $
GH.commitsWithOptionsForR
(GH.N owner) (GH.N repo) (GH.FetchAtLeast 1)
[GH.CommitQuerySha branch]) >>= \case
Right (toList -> (commit:_)) -> do
let GH.N rev = GH.commitSha commit
setPackageSpecAttr "rev" (String rev)
_ -> pure ()
Nothing -> pure ()
-- Figures out the URL template
whenNotSet "url_template" $
setPackageSpecAttr "url_template" (String $ T.pack githubURLTemplate)
-- Figures out the URL from the template
withPackageSpecAttr "url_template" (\case
String (T.unpack -> template) -> do
(_, packageSpec) <- get
let stringValues = packageSpecStringValues packageSpec
case renderTemplate stringValues template of
Just renderedURL ->
setPackageSpecAttr "url" (String $ T.pack renderedURL)
Nothing -> pure ()
_ -> pure ()
)
where
githubURLTemplate :: String
githubURLTemplate =
"https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
2019-01-24 23:58:22 +03:00
2019-01-23 23:55:26 +03:00
-------------------------------------------------------------------------------
-- SHOW
-------------------------------------------------------------------------------
2019-01-18 01:00:48 +03:00
2019-01-24 23:58:22 +03:00
parseCmdShow :: ParserInfo (IO ())
parseCmdShow = info (pure cmdShow <**> helper) fullDesc
2019-01-18 01:00:48 +03:00
cmdShow :: IO ()
2019-01-23 23:55:26 +03:00
cmdShow = do
putStrLn $ "Showing versions file"
VersionsSpec fileVersionsValue <- getVersionsSpec
forWithKeyM_ fileVersionsValue $ \key (PackageSpec spec) -> do
putStrLn $ "Package: " <> unPackageName key
forM_ (HMap.toList spec) $ \(attrName, attrValValue) -> do
let attrValue = case attrValValue of
String str -> str
_ -> "<barabajagal>"
putStrLn $ " " <> T.unpack attrName <> ": " <> T.unpack attrValue
-------------------------------------------------------------------------------
-- UPDATE
-------------------------------------------------------------------------------
2019-01-18 01:00:48 +03:00
2019-01-24 23:58:22 +03:00
parseCmdUpdate :: ParserInfo (IO ())
2019-01-27 01:39:38 +03:00
parseCmdUpdate = info ((cmdUpdate <$> parsePackage) <**> helper) fullDesc
2019-01-24 23:58:22 +03:00
2019-01-27 01:39:38 +03:00
cmdUpdate :: (PackageName, PackageSpec) -> IO ()
cmdUpdate pkgs = do
2019-01-23 23:55:26 +03:00
putStrLn $ "Updating versions file"
VersionsSpec fileVersionsValue <- getVersionsSpec
fileVersionsValue' <- forWithKeyM fileVersionsValue $ \key spec -> do
putStrLn $ "Package: " <> unPackageName key
2019-01-27 01:39:38 +03:00
-- TODO: use StateT
-- let packageUrl <- renderTemplate
2019-01-23 23:55:26 +03:00
2019-01-27 01:39:38 +03:00
-- putStrLn $ " URL: " <> packageUrl
2019-01-23 23:55:26 +03:00
2019-01-27 01:39:38 +03:00
-- sha256 <- nixPrefetchURL packageUrl
2019-01-23 23:55:26 +03:00
2019-01-27 01:39:38 +03:00
-- putStrLn $ " SHA256: " <> sha256
2019-01-23 23:55:26 +03:00
putStrLn $ "Writing new versions file"
encodeFile fileVersionsJson fileVersionsValue'
2019-01-18 01:00:48 +03:00
2019-01-23 23:55:26 +03:00
parseCommand :: Parser (IO ())
parseCommand = subparser (
command "init" parseCmdInit <>
2019-01-24 23:58:22 +03:00
command "add" parseCmdAdd <>
command "show" parseCmdShow <>
command "update" parseCmdUpdate )
2019-01-18 01:00:48 +03:00
main :: IO ()
2019-01-23 23:55:26 +03:00
main = join $ execParser opts
where
opts = info (parseCommand <**> helper)
( fullDesc
<> header "NIV - Nix Version manager" )
nixPrefetchURL :: String -> IO String
nixPrefetchURL = pure
-------------------------------------------------------------------------------
-- Aux
-------------------------------------------------------------------------------
--- Aeson
-- | Efficiently deserialize a JSON value from a file.
-- If this fails due to incomplete or invalid input, 'Nothing' is
-- returned.
--
-- The input file's content must consist solely of a JSON document,
-- with no trailing data except for whitespace.
--
-- This function parses immediately, but defers conversion. See
-- 'json' for details.
decodeFileStrict :: (FromJSON a) => FilePath -> IO (Maybe a)
decodeFileStrict = fmap decodeStrict . B.readFile
-- | Efficiently serialize a JSON value as a lazy 'L.ByteString' and write it to a file.
encodeFile :: (ToJSON a) => FilePath -> a -> IO ()
encodeFile fp = L.writeFile fp . encode
--- HashMap
forWithKeyM
:: (Eq k, Hashable k, Monad m)
=> HMap.HashMap k v1
-> (k -> v1 -> m v2)
-> m (HMap.HashMap k v2)
forWithKeyM = flip mapWithKeyM
forWithKeyM_
:: (Eq k, Hashable k, Monad m)
=> HMap.HashMap k v1
-> (k -> v1 -> m ())
-> m ()
forWithKeyM_ = flip mapWithKeyM_
mapWithKeyM
:: (Eq k, Hashable k, Monad m)
=> (k -> v1 -> m v2)
-> HMap.HashMap k v1
-> m (HMap.HashMap k v2)
mapWithKeyM f m = do
fmap mconcat $ forM (HMap.toList m) $ \(k, v) ->
HMap.singleton k <$> f k v
mapWithKeyM_
:: (Eq k, Hashable k, Monad m)
=> (k -> v1 -> m ())
-> HMap.HashMap k v1
-> m ()
mapWithKeyM_ f m = do
forM_ (HMap.toList m) $ \(k, v) ->
HMap.singleton k <$> f k v
2019-01-27 01:39:38 +03:00
-- | Renders the template. Returns 'Nothing' if some of the attributes are
-- missing.
--
-- renderTemplate [("foo", "bar")] "<foo>" == Just "bar"
-- renderTemplate [("foo", "bar")] "<baz>" == Nothing
renderTemplate :: [(String, String)] -> String -> Maybe String
renderTemplate vals = \case
'<':str -> do
case span (/= '>') str of
(key, '>':rest) ->
liftA2 (<>) (lookup key vals) (renderTemplate vals rest)
c:str -> (c:) <$> renderTemplate vals str
[] -> Just []