2019-01-23 23:55:26 +03:00
|
|
|
{-# LANGUAGE DerivingStrategies #-}
|
|
|
|
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
|
|
|
|
{-# LANGUAGE LambdaCase #-}
|
|
|
|
{-# LANGUAGE OverloadedStrings #-}
|
2019-01-28 23:25:09 +03:00
|
|
|
{-# LANGUAGE QuasiQuotes #-}
|
2019-01-24 23:58:22 +03:00
|
|
|
{-# LANGUAGE TupleSections #-}
|
|
|
|
{-# LANGUAGE ViewPatterns #-}
|
2019-01-23 23:55:26 +03:00
|
|
|
|
2019-01-28 23:25:09 +03:00
|
|
|
{-# OPTIONS_GHC "-Wall" #-}
|
|
|
|
|
|
|
|
module Main (main) where
|
|
|
|
|
2019-01-28 23:29:35 +03:00
|
|
|
import Control.Applicative
|
2019-01-18 01:00:48 +03:00
|
|
|
import Control.Monad
|
2019-01-27 01:39:38 +03:00
|
|
|
import Control.Monad.State
|
2019-01-28 23:29:35 +03:00
|
|
|
import Data.Aeson (FromJSON, FromJSONKey, ToJSON, ToJSONKey)
|
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 23:18:10 +03:00
|
|
|
import Data.Maybe (mapMaybe, fromMaybe)
|
2019-01-30 20:50:09 +03:00
|
|
|
import Data.Semigroup
|
2019-01-28 23:29:35 +03:00
|
|
|
import Data.String.QQ (s)
|
2019-01-27 01:39:38 +03:00
|
|
|
import GHC.Exts (toList)
|
2019-01-28 23:51:10 +03:00
|
|
|
import System.Exit (exitFailure)
|
2019-01-28 23:29:35 +03:00
|
|
|
import System.FilePath ((</>), takeDirectory)
|
2019-01-27 23:18:10 +03:00
|
|
|
import System.Process (readProcess)
|
2019-01-28 23:29:35 +03:00
|
|
|
import qualified Data.Aeson as Aeson
|
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
|
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-27 23:18:10 +03:00
|
|
|
import qualified Options.Applicative as Opts
|
2019-01-29 00:32:36 +03:00
|
|
|
import qualified Options.Applicative.Help.Pretty as Opts
|
2019-01-28 23:29:35 +03:00
|
|
|
import qualified System.Directory as Dir
|
2019-01-18 01:00:48 +03:00
|
|
|
|
2019-01-29 00:32:36 +03:00
|
|
|
main :: IO ()
|
|
|
|
main = join $ Opts.execParser opts
|
|
|
|
where
|
|
|
|
opts = Opts.info (parseCommand <**> Opts.helper) $ mconcat desc
|
|
|
|
desc =
|
|
|
|
[ Opts.fullDesc
|
|
|
|
, Opts.header "NIV - Version manager for Nix projects"
|
|
|
|
]
|
|
|
|
|
|
|
|
parseCommand :: Opts.Parser (IO ())
|
|
|
|
parseCommand = Opts.subparser (
|
|
|
|
Opts.command "init" parseCmdInit <>
|
|
|
|
Opts.command "add" parseCmdAdd <>
|
|
|
|
Opts.command "show" parseCmdShow <>
|
|
|
|
Opts.command "update" parseCmdUpdate <>
|
|
|
|
Opts.command "drop" parseCmdDrop )
|
|
|
|
|
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
|
2019-01-30 20:42:14 +03:00
|
|
|
-- TODO: if doesn't exist: run niv init
|
2019-01-23 23:55:26 +03:00
|
|
|
putStrLn $ "Reading versions file"
|
2019-01-28 23:25:09 +03:00
|
|
|
decodeFileStrict pathNixVersionsJson >>= \case
|
2019-01-28 23:29:35 +03:00
|
|
|
Just (Aeson.Object obj) ->
|
2019-01-23 23:55:26 +03:00
|
|
|
fmap (VersionsSpec . mconcat) $
|
2019-01-27 23:18:10 +03:00
|
|
|
forM (HMap.toList obj) $ \(k, v) ->
|
2019-01-23 23:55:26 +03:00
|
|
|
case v of
|
2019-01-28 23:29:35 +03:00
|
|
|
Aeson.Object v' ->
|
2019-01-23 23:55:26 +03:00
|
|
|
pure $ HMap.singleton (PackageName (T.unpack k)) (PackageSpec v')
|
2019-01-28 23:51:10 +03:00
|
|
|
_ -> abortAttributeIsntAMap
|
|
|
|
Just _ -> abortVersionsIsntAMap
|
|
|
|
Nothing -> abortVersionsIsntJSON
|
2019-01-23 23:55:26 +03:00
|
|
|
|
2019-01-31 01:56:35 +03:00
|
|
|
-- TODO: pretty
|
2019-01-27 23:18:10 +03:00
|
|
|
setVersionsSpec :: VersionsSpec -> IO ()
|
2019-01-28 23:25:09 +03:00
|
|
|
setVersionsSpec versionsSpec = encodeFile pathNixVersionsJson versionsSpec
|
2019-01-27 23:18:10 +03:00
|
|
|
|
2019-01-23 23:55:26 +03:00
|
|
|
newtype PackageName = PackageName { unPackageName :: String }
|
2019-01-24 23:58:22 +03:00
|
|
|
deriving newtype (Eq, Hashable, FromJSONKey, ToJSONKey, Show)
|
|
|
|
|
2019-01-27 23:18:10 +03:00
|
|
|
parsePackageName :: Opts.Parser PackageName
|
|
|
|
parsePackageName = PackageName <$>
|
|
|
|
Opts.argument Opts.str (Opts.metavar "PACKAGE")
|
2019-01-23 23:55:26 +03:00
|
|
|
|
2019-01-28 23:29:35 +03:00
|
|
|
newtype PackageSpec = PackageSpec { unPackageSpec :: Aeson.Object }
|
2019-01-30 20:50:09 +03:00
|
|
|
deriving newtype (FromJSON, ToJSON, Show, Semigroup, Monoid)
|
2019-01-23 23:55:26 +03:00
|
|
|
|
2019-01-27 23:18:10 +03:00
|
|
|
parsePackageSpec :: Opts.Parser PackageSpec
|
2019-01-24 23:58:22 +03:00
|
|
|
parsePackageSpec =
|
|
|
|
(PackageSpec . HMap.fromList . fmap fixupAttributes) <$>
|
|
|
|
many parseAttribute
|
|
|
|
where
|
2019-01-27 23:18:10 +03:00
|
|
|
parseAttribute :: Opts.Parser (String, String)
|
2019-01-24 23:58:22 +03:00
|
|
|
parseAttribute = shortcutAttributes <|>
|
2019-01-27 23:18:10 +03:00
|
|
|
Opts.option (Opts.maybeReader parseKeyVal)
|
|
|
|
( Opts.long "attribute" <>
|
|
|
|
Opts.short 'a' <>
|
|
|
|
Opts.metavar "KEY=VAL"
|
2019-01-29 00:32:36 +03:00
|
|
|
) <|>
|
|
|
|
(("url_template",) <$> Opts.strOption
|
|
|
|
( Opts.long "template" <>
|
|
|
|
Opts.short 't' <>
|
|
|
|
Opts.metavar "URL" <>
|
2019-01-29 00:39:08 +03:00
|
|
|
Opts.help "Used during 'update' when building URL. Occurrences of <foo> are replaced with attribute 'foo'."
|
2019-01-29 00:32:36 +03:00
|
|
|
))
|
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
|
2019-01-27 23:18:10 +03:00
|
|
|
shortcutAttributes :: Opts.Parser (String, String)
|
2019-01-24 23:58:22 +03:00
|
|
|
shortcutAttributes = foldr (<|>) empty $ mkShortcutAttribute <$>
|
2019-01-29 00:32:36 +03:00
|
|
|
[ "branch", "owner", "repo", "version" ]
|
2019-01-24 23:58:22 +03:00
|
|
|
|
2019-01-27 23:18:10 +03:00
|
|
|
mkShortcutAttribute :: String -> Opts.Parser (String, String)
|
|
|
|
mkShortcutAttribute = \case
|
|
|
|
attr@(c:_) -> (attr,) <$> Opts.strOption
|
|
|
|
( Opts.long attr <> Opts.short c <> Opts.metavar (toUpper <$> attr) )
|
2019-01-28 23:51:10 +03:00
|
|
|
_ -> empty
|
2019-01-24 23:58:22 +03:00
|
|
|
|
2019-01-28 23:29:35 +03:00
|
|
|
fixupAttributes :: (String, String) -> (T.Text, Aeson.Value)
|
|
|
|
fixupAttributes (k, v) = (T.pack k, Aeson.String (T.pack v))
|
2019-01-24 23:58:22 +03:00
|
|
|
|
2019-01-27 23:18:10 +03:00
|
|
|
parsePackage :: Opts.Parser (PackageName, PackageSpec)
|
2019-01-24 23:58:22 +03:00
|
|
|
parsePackage = (,) <$> parsePackageName <*> parsePackageSpec
|
|
|
|
|
2019-01-27 23:18:10 +03:00
|
|
|
-------------------------------------------------------------------------------
|
|
|
|
-- PACKAGE SPEC OPS
|
|
|
|
-------------------------------------------------------------------------------
|
|
|
|
|
|
|
|
updatePackageSpec :: PackageSpec -> IO PackageSpec
|
|
|
|
updatePackageSpec = execStateT $ do
|
|
|
|
-- Figures out the URL from the template
|
|
|
|
withPackageSpecAttr "url_template" (\case
|
2019-01-28 23:29:35 +03:00
|
|
|
Aeson.String (T.unpack -> template) -> do
|
2019-01-27 23:18:10 +03:00
|
|
|
packageSpec <- get
|
|
|
|
let stringValues = packageSpecStringValues packageSpec
|
|
|
|
case renderTemplate stringValues template of
|
|
|
|
Just renderedURL ->
|
2019-01-28 23:29:35 +03:00
|
|
|
setPackageSpecAttr "url" (Aeson.String $ T.pack renderedURL)
|
2019-01-27 23:18:10 +03:00
|
|
|
Nothing -> pure ()
|
|
|
|
_ -> pure ()
|
|
|
|
)
|
|
|
|
|
|
|
|
-- Updates the sha256 based on the URL contents
|
|
|
|
withPackageSpecAttr "url" (\case
|
2019-01-28 23:29:35 +03:00
|
|
|
Aeson.String (T.unpack -> url) -> do
|
2019-01-27 23:18:10 +03:00
|
|
|
sha256 <- liftIO $ nixPrefetchURL url
|
2019-01-28 23:29:35 +03:00
|
|
|
setPackageSpecAttr "sha256" (Aeson.String $ T.pack sha256)
|
2019-01-27 23:18:10 +03:00
|
|
|
_ -> pure ()
|
|
|
|
)
|
|
|
|
|
|
|
|
completePackageSpec
|
|
|
|
:: PackageSpec
|
|
|
|
-> IO (PackageSpec)
|
|
|
|
completePackageSpec = execStateT $ do
|
|
|
|
|
|
|
|
-- In case we have @owner@ and @repo@, pull some data from GitHub
|
|
|
|
(,) <$> getPackageSpecAttr "owner" <*> getPackageSpecAttr "repo" >>= \case
|
2019-01-28 23:29:35 +03:00
|
|
|
(Just (Aeson.String owner), Just (Aeson.String repo)) -> do
|
2019-01-27 23:18:10 +03:00
|
|
|
liftIO (GH.executeRequest' $ GH.repositoryR (GH.N owner) (GH.N repo))
|
|
|
|
>>= \case
|
|
|
|
Left _ -> pure ()
|
|
|
|
Right ghRepo -> do
|
|
|
|
|
|
|
|
-- Description
|
|
|
|
whenNotSet "description" $ case GH.repoDescription ghRepo of
|
2019-01-28 23:29:35 +03:00
|
|
|
Just descr ->
|
|
|
|
setPackageSpecAttr "description" (Aeson.String descr)
|
2019-01-27 23:18:10 +03:00
|
|
|
Nothing -> pure ()
|
|
|
|
|
2019-01-30 21:18:41 +03:00
|
|
|
whenNotSet "homepage" $ case GH.repoHomepage ghRepo of
|
|
|
|
Just descr ->
|
|
|
|
setPackageSpecAttr "homepage" (Aeson.String descr)
|
|
|
|
Nothing -> pure ()
|
|
|
|
|
2019-01-27 23:18:10 +03:00
|
|
|
-- Branch and rev
|
|
|
|
whenNotSet "branch" $ case GH.repoDefaultBranch ghRepo of
|
2019-01-28 23:29:35 +03:00
|
|
|
Just branch ->
|
|
|
|
setPackageSpecAttr "branch" (Aeson.String branch)
|
2019-01-27 23:18:10 +03:00
|
|
|
Nothing -> pure ()
|
|
|
|
|
|
|
|
withPackageSpecAttr "branch" (\case
|
2019-01-28 23:29:35 +03:00
|
|
|
Aeson.String branch -> do
|
2019-01-27 23:18:10 +03:00
|
|
|
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
|
2019-01-28 23:29:35 +03:00
|
|
|
setPackageSpecAttr "rev" (Aeson.String rev)
|
2019-01-27 23:18:10 +03:00
|
|
|
_ -> pure ()
|
|
|
|
_ -> pure ()
|
|
|
|
)
|
|
|
|
(_,_) -> pure ()
|
|
|
|
|
|
|
|
-- Figures out the URL template
|
|
|
|
whenNotSet "url_template" $
|
2019-01-28 23:29:35 +03:00
|
|
|
setPackageSpecAttr
|
|
|
|
"url_template"
|
|
|
|
(Aeson.String $ T.pack githubURLTemplate)
|
2019-01-27 23:18:10 +03:00
|
|
|
|
|
|
|
where
|
|
|
|
githubURLTemplate :: String
|
|
|
|
githubURLTemplate =
|
|
|
|
"https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
|
|
|
|
|
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
|
2019-01-27 23:18:10 +03:00
|
|
|
-> StateT PackageSpec IO ()
|
|
|
|
-> StateT PackageSpec IO ()
|
2019-01-27 01:39:38 +03:00
|
|
|
whenNotSet attrName act = getPackageSpecAttr attrName >>= \case
|
|
|
|
Just _ -> pure ()
|
|
|
|
Nothing -> act
|
|
|
|
|
|
|
|
withPackageSpecAttr
|
|
|
|
:: T.Text
|
2019-01-28 23:29:35 +03:00
|
|
|
-> (Aeson.Value -> StateT PackageSpec IO ())
|
2019-01-27 23:18:10 +03:00
|
|
|
-> StateT PackageSpec IO ()
|
2019-01-27 01:39:38 +03:00
|
|
|
withPackageSpecAttr attrName act = getPackageSpecAttr attrName >>= \case
|
|
|
|
Just v -> act v
|
|
|
|
Nothing -> pure ()
|
|
|
|
|
|
|
|
getPackageSpecAttr
|
|
|
|
:: T.Text
|
2019-01-28 23:29:35 +03:00
|
|
|
-> StateT PackageSpec IO (Maybe Aeson.Value)
|
2019-01-27 01:39:38 +03:00
|
|
|
getPackageSpecAttr attrName = do
|
2019-01-27 23:18:10 +03:00
|
|
|
PackageSpec obj <- get
|
2019-01-27 01:39:38 +03:00
|
|
|
pure $ HMap.lookup attrName obj
|
|
|
|
|
|
|
|
setPackageSpecAttr
|
2019-01-28 23:29:35 +03:00
|
|
|
:: T.Text -> Aeson.Value
|
2019-01-27 23:18:10 +03:00
|
|
|
-> StateT PackageSpec IO ()
|
2019-01-27 01:39:38 +03:00
|
|
|
setPackageSpecAttr attrName attrValue = do
|
2019-01-27 23:18:10 +03:00
|
|
|
PackageSpec obj <- get
|
2019-01-27 01:39:38 +03:00
|
|
|
let obj' = HMap.insert attrName attrValue obj
|
2019-01-27 23:18:10 +03:00
|
|
|
put (PackageSpec obj')
|
2019-01-27 01:39:38 +03:00
|
|
|
|
|
|
|
packageSpecStringValues :: PackageSpec -> [(String, String)]
|
|
|
|
packageSpecStringValues (PackageSpec m) = mapMaybe toVal (HMap.toList m)
|
|
|
|
where
|
2019-01-28 23:29:35 +03:00
|
|
|
toVal :: (T.Text, Aeson.Value) -> Maybe (String, String)
|
2019-01-27 01:39:38 +03:00
|
|
|
toVal = \case
|
2019-01-28 23:29:35 +03:00
|
|
|
(key, Aeson.String val) -> Just (T.unpack key, T.unpack val)
|
2019-01-27 01:39:38 +03:00
|
|
|
_ -> Nothing
|
2019-01-23 23:55:26 +03:00
|
|
|
|
|
|
|
-------------------------------------------------------------------------------
|
|
|
|
-- INIT
|
|
|
|
-------------------------------------------------------------------------------
|
|
|
|
|
2019-01-27 23:18:10 +03:00
|
|
|
parseCmdInit :: Opts.ParserInfo (IO ())
|
2019-01-29 00:32:36 +03:00
|
|
|
parseCmdInit = Opts.info (pure cmdInit <**> Opts.helper) $ mconcat desc
|
|
|
|
where
|
|
|
|
desc =
|
|
|
|
[ Opts.fullDesc
|
|
|
|
, Opts.progDesc
|
|
|
|
"Initialize a Nix project. Existing files won't be modified."
|
|
|
|
]
|
2019-01-23 23:55:26 +03:00
|
|
|
|
2019-01-18 01:00:48 +03:00
|
|
|
cmdInit :: IO ()
|
|
|
|
cmdInit = do
|
2019-01-28 23:25:09 +03:00
|
|
|
|
|
|
|
-- Writes all the default files
|
|
|
|
forM_
|
|
|
|
[ (pathNixVersionsJson, initNixVersionsJsonContent)
|
|
|
|
, (pathNixFetchNix, initNixFetchNixContent)
|
|
|
|
, (pathNixDefaultNix, initNixDefaultNixContent)
|
|
|
|
, (pathDefaultNix, initDefaultNixContent)
|
|
|
|
, (pathShellNix, initShellNixContent)
|
|
|
|
] $ \(path, content) -> do
|
|
|
|
putStrLn $ "Creating file " <> path <> " (if it doesn't exist)"
|
|
|
|
let dir = takeDirectory path
|
2019-01-28 23:29:35 +03:00
|
|
|
Dir.createDirectoryIfMissing True dir
|
|
|
|
exists <- Dir.doesFileExist path
|
2019-01-28 23:25:09 +03:00
|
|
|
if exists
|
|
|
|
then do
|
|
|
|
putStrLn $ "Not creating " <> path <> " (already exists)"
|
|
|
|
else do
|
|
|
|
putStrLn $ "Creating " <> path <> " (doesn't exist)"
|
|
|
|
writeFile path content
|
|
|
|
|
|
|
|
-- Imports @niv@ and @nixpkgs@ (18.09)
|
|
|
|
putStrLn "Importing 'niv' ..."
|
|
|
|
cmdAdd (PackageName "nmattia/niv", PackageSpec HMap.empty) Nothing
|
|
|
|
putStrLn "Importing 'nixpkgs' ..."
|
|
|
|
cmdAdd
|
|
|
|
( PackageName "NixOS/nixpkgs-channels"
|
|
|
|
, PackageSpec (HMap.singleton "branch" "nixos-18.09"))
|
|
|
|
(Just (PackageName "nixpkgs"))
|
2019-01-18 01:00:48 +03:00
|
|
|
|
2019-01-23 23:55:26 +03:00
|
|
|
-------------------------------------------------------------------------------
|
|
|
|
-- ADD
|
|
|
|
-------------------------------------------------------------------------------
|
|
|
|
|
2019-01-27 23:18:10 +03:00
|
|
|
parseCmdAdd :: Opts.ParserInfo (IO ())
|
|
|
|
parseCmdAdd =
|
2019-01-29 00:32:36 +03:00
|
|
|
Opts.info ((cmdAdd <$> parsePackage <*> optName) <**> Opts.helper) $
|
|
|
|
mconcat desc
|
2019-01-27 23:18:10 +03:00
|
|
|
where
|
|
|
|
optName :: Opts.Parser (Maybe PackageName)
|
|
|
|
optName = Opts.optional $ PackageName <$> Opts.strOption
|
|
|
|
( Opts.long "name" <>
|
|
|
|
Opts.short 'n' <>
|
|
|
|
Opts.metavar "NAME"
|
|
|
|
)
|
2019-01-29 00:32:36 +03:00
|
|
|
desc =
|
|
|
|
[ Opts.fullDesc
|
|
|
|
, Opts.progDesc "Add dependency"
|
|
|
|
, Opts.headerDoc $ Just $
|
|
|
|
"Examples:" Opts.<$$>
|
|
|
|
"" Opts.<$$>
|
|
|
|
" niv add stedolan/jq" Opts.<$$>
|
|
|
|
" niv add NixOS/nixpkgs-channel -n nixpkgs -b nixos-18.09" Opts.<$$>
|
|
|
|
" niv add my-package -v alpha-0.1 -t http://example.com/archive/<version>.zip"
|
|
|
|
]
|
2019-01-23 23:55:26 +03:00
|
|
|
|
2019-01-27 23:18:10 +03:00
|
|
|
cmdAdd :: (PackageName, PackageSpec) -> Maybe PackageName -> IO ()
|
|
|
|
cmdAdd (PackageName str, spec) mPackageName = do
|
2019-01-27 01:39:38 +03:00
|
|
|
|
|
|
|
-- Figures out the owner and repo
|
2019-01-27 23:18:10 +03:00
|
|
|
(packageName, spec') <- flip runStateT spec $ case span (/= '/') str of
|
2019-01-27 01:39:38 +03:00
|
|
|
(owner@(_:_), '/':repo@(_:_)) -> do
|
|
|
|
whenNotSet "owner" $
|
2019-01-28 23:29:35 +03:00
|
|
|
setPackageSpecAttr "owner" (Aeson.String $ T.pack owner)
|
2019-01-27 01:39:38 +03:00
|
|
|
whenNotSet "repo" $ do
|
2019-01-28 23:29:35 +03:00
|
|
|
setPackageSpecAttr "repo" (Aeson.String $ T.pack repo)
|
2019-01-27 23:18:10 +03:00
|
|
|
pure (PackageName repo)
|
|
|
|
_ -> pure (PackageName str)
|
2019-01-27 01:39:38 +03:00
|
|
|
|
2019-01-28 23:25:09 +03:00
|
|
|
versionsSpec <- unVersionsSpec <$> getVersionsSpec
|
2019-01-27 01:39:38 +03:00
|
|
|
|
2019-01-27 23:18:10 +03:00
|
|
|
let packageName' = fromMaybe packageName mPackageName
|
2019-01-27 01:39:38 +03:00
|
|
|
|
2019-01-28 23:51:10 +03:00
|
|
|
when (HMap.member packageName' versionsSpec) $
|
|
|
|
abortCannotAddPackageExists packageName'
|
2019-01-27 01:39:38 +03:00
|
|
|
|
2019-01-27 23:18:10 +03:00
|
|
|
spec'' <- updatePackageSpec =<< completePackageSpec spec'
|
2019-01-27 01:39:38 +03:00
|
|
|
|
2019-01-27 23:18:10 +03:00
|
|
|
putStrLn $ "Writing new versions file"
|
|
|
|
setVersionsSpec $ VersionsSpec $
|
|
|
|
HMap.insert packageName' spec'' versionsSpec
|
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-27 23:18:10 +03:00
|
|
|
parseCmdShow :: Opts.ParserInfo (IO ())
|
|
|
|
parseCmdShow = Opts.info (pure cmdShow <**> Opts.helper) Opts.fullDesc
|
2019-01-24 23:58:22 +03:00
|
|
|
|
2019-01-18 01:00:48 +03:00
|
|
|
cmdShow :: IO ()
|
2019-01-23 23:55:26 +03:00
|
|
|
cmdShow = do
|
|
|
|
putStrLn $ "Showing versions file"
|
|
|
|
|
2019-01-28 23:25:09 +03:00
|
|
|
fileVersionsValue <- unVersionsSpec <$> getVersionsSpec
|
2019-01-23 23:55:26 +03:00
|
|
|
|
|
|
|
forWithKeyM_ fileVersionsValue $ \key (PackageSpec spec) -> do
|
|
|
|
putStrLn $ "Package: " <> unPackageName key
|
|
|
|
forM_ (HMap.toList spec) $ \(attrName, attrValValue) -> do
|
|
|
|
let attrValue = case attrValValue of
|
2019-01-28 23:29:35 +03:00
|
|
|
Aeson.String str -> str
|
2019-01-23 23:55:26 +03:00
|
|
|
_ -> "<barabajagal>"
|
|
|
|
putStrLn $ " " <> T.unpack attrName <> ": " <> T.unpack attrValue
|
|
|
|
|
|
|
|
-------------------------------------------------------------------------------
|
|
|
|
-- UPDATE
|
|
|
|
-------------------------------------------------------------------------------
|
2019-01-18 01:00:48 +03:00
|
|
|
|
2019-01-27 23:18:10 +03:00
|
|
|
parseCmdUpdate :: Opts.ParserInfo (IO ())
|
|
|
|
parseCmdUpdate =
|
|
|
|
Opts.info
|
2019-01-29 00:32:36 +03:00
|
|
|
((cmdUpdate <$> Opts.optional parsePackage) <**> Opts.helper) $
|
|
|
|
mconcat desc
|
|
|
|
where
|
|
|
|
desc =
|
|
|
|
[ Opts.fullDesc
|
|
|
|
, Opts.progDesc "Update dependencies"
|
|
|
|
, Opts.headerDoc $ Just $
|
|
|
|
"Examples:" Opts.<$$>
|
|
|
|
"" Opts.<$$>
|
|
|
|
" niv update" Opts.<$$>
|
|
|
|
" niv update nixpkgs" Opts.<$$>
|
|
|
|
" niv update my-package -v beta-0.2"
|
|
|
|
]
|
2019-01-24 23:58:22 +03:00
|
|
|
|
2019-01-27 23:18:10 +03:00
|
|
|
cmdUpdate :: Maybe (PackageName, PackageSpec) -> IO ()
|
|
|
|
cmdUpdate = \case
|
|
|
|
Just (packageName, packageSpec) -> do
|
|
|
|
putStrLn $ "Updating single package: " <> unPackageName packageName
|
2019-01-28 23:25:09 +03:00
|
|
|
versionsSpec <- unVersionsSpec <$> getVersionsSpec
|
2019-01-23 23:55:26 +03:00
|
|
|
|
2019-01-27 23:18:10 +03:00
|
|
|
packageSpec' <- case HMap.lookup packageName versionsSpec of
|
|
|
|
Just packageSpec' -> do
|
2019-01-30 20:50:09 +03:00
|
|
|
|
|
|
|
-- TODO: something fishy happening here
|
|
|
|
pkgSpec <- completePackageSpec $ packageSpec <> packageSpec'
|
|
|
|
updatePackageSpec $ pkgSpec
|
|
|
|
|
2019-01-28 23:51:10 +03:00
|
|
|
Nothing -> abortCannotUpdateNoSuchPackage packageName
|
2019-01-23 23:55:26 +03:00
|
|
|
|
2019-01-27 23:18:10 +03:00
|
|
|
setVersionsSpec $ VersionsSpec $
|
|
|
|
HMap.insert packageName packageSpec' versionsSpec
|
2019-01-23 23:55:26 +03:00
|
|
|
|
2019-01-27 23:18:10 +03:00
|
|
|
Nothing -> do
|
2019-01-28 23:25:09 +03:00
|
|
|
versionsSpec <- unVersionsSpec <$> getVersionsSpec
|
2019-01-23 23:55:26 +03:00
|
|
|
|
2019-01-27 23:18:10 +03:00
|
|
|
versionsSpec' <- forWithKeyM versionsSpec $
|
|
|
|
\packageName packageSpec -> do
|
|
|
|
putStrLn $ "Package: " <> unPackageName packageName
|
|
|
|
updatePackageSpec packageSpec
|
2019-01-23 23:55:26 +03:00
|
|
|
|
2019-01-27 23:18:10 +03:00
|
|
|
setVersionsSpec $ VersionsSpec versionsSpec'
|
2019-01-23 23:55:26 +03:00
|
|
|
|
2019-01-27 23:18:10 +03:00
|
|
|
-------------------------------------------------------------------------------
|
|
|
|
-- DROP
|
|
|
|
-------------------------------------------------------------------------------
|
2019-01-23 23:55:26 +03:00
|
|
|
|
2019-01-27 23:18:10 +03:00
|
|
|
parseCmdDrop :: Opts.ParserInfo (IO ())
|
|
|
|
parseCmdDrop =
|
|
|
|
Opts.info
|
2019-01-29 00:32:36 +03:00
|
|
|
((cmdDrop <$> parsePackageName) <**> Opts.helper) $
|
|
|
|
mconcat desc
|
|
|
|
where
|
|
|
|
desc =
|
|
|
|
[ Opts.fullDesc
|
|
|
|
, Opts.progDesc "Drop dependency"
|
|
|
|
, Opts.headerDoc $ Just $
|
|
|
|
"Examples:" Opts.<$$>
|
|
|
|
"" Opts.<$$>
|
|
|
|
" niv drop jq"
|
|
|
|
]
|
2019-01-27 23:18:10 +03:00
|
|
|
|
|
|
|
cmdDrop :: PackageName -> IO ()
|
|
|
|
cmdDrop packageName = do
|
|
|
|
putStrLn $ "Dropping package: " <> unPackageName packageName
|
2019-01-28 23:25:09 +03:00
|
|
|
versionsSpec <- unVersionsSpec <$> getVersionsSpec
|
2019-01-27 23:18:10 +03:00
|
|
|
|
|
|
|
when (not $ HMap.member packageName versionsSpec) $
|
2019-01-28 23:51:10 +03:00
|
|
|
abortCannotDropNoSuchPackage packageName
|
2019-01-18 01:00:48 +03:00
|
|
|
|
2019-01-27 23:18:10 +03:00
|
|
|
setVersionsSpec $ VersionsSpec $
|
|
|
|
HMap.delete packageName versionsSpec
|
|
|
|
|
2019-01-23 23:55:26 +03:00
|
|
|
-------------------------------------------------------------------------------
|
|
|
|
-- 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)
|
2019-01-28 23:29:35 +03:00
|
|
|
decodeFileStrict = fmap Aeson.decodeStrict . B.readFile
|
2019-01-23 23:55:26 +03:00
|
|
|
|
|
|
|
-- | Efficiently serialize a JSON value as a lazy 'L.ByteString' and write it to a file.
|
|
|
|
encodeFile :: (ToJSON a) => FilePath -> a -> IO ()
|
2019-01-28 23:29:35 +03:00
|
|
|
encodeFile fp = L.writeFile fp . Aeson.encode
|
2019-01-23 23:55:26 +03:00
|
|
|
|
|
|
|
--- 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)
|
2019-01-27 23:18:10 +03:00
|
|
|
_ -> Nothing
|
2019-01-27 01:39:38 +03:00
|
|
|
c:str -> (c:) <$> renderTemplate vals str
|
|
|
|
[] -> Just []
|
2019-01-28 23:25:09 +03:00
|
|
|
|
2019-01-28 23:51:10 +03:00
|
|
|
abort :: String -> IO a
|
|
|
|
abort msg = do
|
|
|
|
putStrLn msg
|
|
|
|
exitFailure
|
|
|
|
|
2019-01-29 00:32:36 +03:00
|
|
|
nixPrefetchURL :: String -> IO String
|
|
|
|
nixPrefetchURL url =
|
|
|
|
lines <$> readProcess "nix-prefetch-url" ["--unpack", url] "" >>=
|
|
|
|
\case
|
|
|
|
(l:_) -> pure l
|
|
|
|
_ -> abortNixPrefetchExpectedOutput
|
|
|
|
|
2019-01-28 23:25:09 +03:00
|
|
|
-------------------------------------------------------------------------------
|
|
|
|
-- Files and their content
|
|
|
|
-------------------------------------------------------------------------------
|
|
|
|
|
|
|
|
-- | @nix/fetch.nix@
|
|
|
|
pathNixFetchNix :: FilePath
|
|
|
|
pathNixFetchNix = "nix" </> "fetch.nix"
|
|
|
|
|
|
|
|
-- | Glue code between nix and versions.json
|
|
|
|
initNixFetchNixContent :: String
|
|
|
|
initNixFetchNixContent = [s|
|
|
|
|
# A record, from name to path, of the third-party packages
|
|
|
|
let
|
|
|
|
versions = builtins.fromJSON (builtins.readFile ./versions.json);
|
|
|
|
fetchTarball =
|
|
|
|
# fetchTarball version that is compatible between all the versions of
|
|
|
|
# Nix
|
|
|
|
{ url, sha256 }@attrs:
|
|
|
|
let
|
|
|
|
inherit (builtins) lessThan nixVersion fetchTarball;
|
|
|
|
in
|
|
|
|
if lessThan nixVersion "1.12" then
|
|
|
|
fetchTarball { inherit url; }
|
|
|
|
else
|
|
|
|
fetchTarball attrs;
|
|
|
|
in
|
|
|
|
builtins.mapAttrs (_: spec:
|
|
|
|
fetchTarball {
|
|
|
|
url =
|
|
|
|
with spec;
|
|
|
|
"https://github.com/${owner}/${repo}/archive/${rev}.tar.gz";
|
|
|
|
sha256 = spec.sha256;
|
|
|
|
}
|
|
|
|
) versions
|
|
|
|
|]
|
|
|
|
|
|
|
|
-- | @nix/default.nix@
|
|
|
|
pathNixDefaultNix :: FilePath
|
|
|
|
pathNixDefaultNix = "nix" </> "default.nix"
|
|
|
|
|
|
|
|
-- | File importing @nixpkgs@, setting up overlays, etc
|
|
|
|
initNixDefaultNixContent :: String
|
|
|
|
initNixDefaultNixContent = [s|
|
|
|
|
with { fetch = import ./fetch.nix; };
|
|
|
|
import fetch.nixpkgs
|
|
|
|
{ overlays =
|
|
|
|
[ (self: super:
|
|
|
|
{ niv = import fetch.niv {};
|
|
|
|
}
|
|
|
|
)
|
|
|
|
] ;
|
|
|
|
config = { } ;
|
|
|
|
}
|
|
|
|
|]
|
|
|
|
|
|
|
|
-- | @default.nix@
|
|
|
|
pathDefaultNix :: FilePath
|
|
|
|
pathDefaultNix = "default.nix"
|
|
|
|
|
|
|
|
-- | Top level @default.nix@
|
|
|
|
initDefaultNixContent :: String
|
|
|
|
initDefaultNixContent = [s|
|
|
|
|
let pkgs = import ./nix; in pkgs.hello
|
|
|
|
|]
|
|
|
|
|
|
|
|
-- | @shell.nix@
|
|
|
|
pathShellNix :: FilePath
|
|
|
|
pathShellNix = "shell.nix"
|
|
|
|
|
|
|
|
-- | Simple shell that loads @niv@
|
|
|
|
initShellNixContent :: String
|
|
|
|
initShellNixContent = [s|
|
|
|
|
let pkgs = import ./nix;
|
|
|
|
in pkgs.mkShell
|
|
|
|
{ buildInputs = [ pkgs.niv ];
|
|
|
|
}
|
|
|
|
|]
|
|
|
|
|
|
|
|
-- | @nix/versions.json"
|
|
|
|
pathNixVersionsJson :: FilePath
|
|
|
|
pathNixVersionsJson = "nix" </> "versions.json"
|
|
|
|
|
|
|
|
-- | Empty JSON map
|
|
|
|
initNixVersionsJsonContent :: String
|
|
|
|
initNixVersionsJsonContent = "{}"
|
2019-01-28 23:51:10 +03:00
|
|
|
|
|
|
|
-------------------------------------------------------------------------------
|
|
|
|
-- Abort
|
|
|
|
-------------------------------------------------------------------------------
|
|
|
|
|
|
|
|
abortVersionsIsntAMap :: IO a
|
|
|
|
abortVersionsIsntAMap = abort $ unlines [ line1, line2 ]
|
|
|
|
where
|
|
|
|
line1 = "Cannot use " <> pathNixVersionsJson
|
|
|
|
line2 = [s|
|
|
|
|
The versions file should be a JSON map from package name to package
|
|
|
|
specification, e.g.:
|
|
|
|
{ ... }
|
|
|
|
|]
|
|
|
|
|
|
|
|
abortAttributeIsntAMap :: IO a
|
|
|
|
abortAttributeIsntAMap = abort $ unlines [ line1, line2 ]
|
|
|
|
where
|
|
|
|
line1 = "Cannot use " <> pathNixVersionsJson
|
|
|
|
line2 = [s|
|
|
|
|
The package specifications in the versions file should be JSON maps from
|
|
|
|
attribute name to attribute value, e.g.:
|
|
|
|
{ "nixpkgs": { "foo": "bar" } }
|
|
|
|
|]
|
|
|
|
|
|
|
|
abortVersionsIsntJSON :: IO a
|
|
|
|
abortVersionsIsntJSON = abort $ unlines [ line1, line2 ]
|
|
|
|
where
|
|
|
|
line1 = "Cannot use " <> pathNixVersionsJson
|
|
|
|
line2 = "The versions file should be JSON."
|
|
|
|
|
|
|
|
abortCannotAddPackageExists :: PackageName -> IO a
|
|
|
|
abortCannotAddPackageExists (PackageName n) = abort $ unlines
|
|
|
|
[ "Cannot add package " <> n <> "."
|
|
|
|
, "The package already exists. Use"
|
|
|
|
, " nix drop " <> n
|
|
|
|
, "and then re-add the package. Alternatively use"
|
|
|
|
, " nix update " <> n <> " --attr foo=bar"
|
|
|
|
, "to update the package's attributes."
|
|
|
|
]
|
|
|
|
|
|
|
|
abortCannotUpdateNoSuchPackage :: PackageName -> IO a
|
|
|
|
abortCannotUpdateNoSuchPackage (PackageName n) = abort $ unlines
|
|
|
|
[ "Cannot update package " <> n <> "."
|
|
|
|
, "The package doesn't exist. Use"
|
|
|
|
, " nix add " <> n
|
|
|
|
, "to add the package."
|
|
|
|
]
|
|
|
|
|
|
|
|
abortCannotDropNoSuchPackage :: PackageName -> IO a
|
|
|
|
abortCannotDropNoSuchPackage (PackageName n) = abort $ unlines
|
|
|
|
[ "Cannot drop package " <> n <> "."
|
|
|
|
, "The package doesn't exist."
|
|
|
|
]
|
|
|
|
|
|
|
|
abortNixPrefetchExpectedOutput :: IO a
|
|
|
|
abortNixPrefetchExpectedOutput = abort [s|
|
|
|
|
Could not read the output of 'nix-prefetch-url'. This is a bug. Please create a
|
|
|
|
ticket:
|
|
|
|
|
|
|
|
https://github.com/nmattia/niv/issues/new
|
|
|
|
|
|
|
|
Thanks! I'll buy you a beer.
|
|
|
|
|]
|