1
1
mirror of https://github.com/nmattia/niv.git synced 2024-11-08 08:26:02 +03:00
niv/Main.hs

781 lines
25 KiB
Haskell
Raw Normal View History

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
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)
import Data.Functor ((<&>))
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-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-02-07 16:15:56 +03:00
import qualified Data.Aeson.Encode.Pretty as AesonPretty
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 )
newtype Sources = Sources
{ unSources :: HMap.HashMap PackageName PackageSpec }
2019-01-24 23:58:22 +03:00
deriving newtype (FromJSON, ToJSON)
getSources :: IO Sources
getSources = do
2019-01-30 20:42:14 +03:00
-- TODO: if doesn't exist: run niv init
putStrLn $ "Reading sources file"
decodeFileStrict pathNixSourcesJson >>= \case
2019-01-28 23:29:35 +03:00
Just (Aeson.Object obj) ->
fmap (Sources . 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 _ -> abortSourcesIsntAMap
Nothing -> abortSourcesIsntJSON
2019-01-23 23:55:26 +03:00
2019-01-31 01:56:35 +03:00
-- TODO: pretty
setSources :: Sources -> IO ()
setSources sources = encodeFile pathNixSourcesJson sources
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-31 22:42:39 +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-31 02:05:35 +03:00
parseAttribute =
2019-01-27 23:18:10 +03:00
Opts.option (Opts.maybeReader parseKeyVal)
( Opts.long "attribute" <>
Opts.short 'a' <>
2019-01-31 02:05:35 +03:00
Opts.metavar "KEY=VAL" <>
Opts.help "Set the package spec attribute <KEY> to <VAL>"
) <|> shortcutAttributes <|>
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'."
)) <|>
(("type",) <$> Opts.strOption
( Opts.long "type" <>
Opts.short 'T' <>
Opts.metavar "TYPE" <>
Opts.help "The type of the URL target. The value can be either 'file' or 'tarball'. If not set, the value is inferred from the suffix of the URL."
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
2019-01-31 02:05:35 +03:00
( Opts.long attr <>
Opts.short c <>
Opts.metavar (toUpper <$> attr) <>
Opts.help
(
"Equivalent to --attribute " <>
attr <> "=<" <> (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
2019-02-22 20:54:51 +03:00
originalUrl <- getPackageSpecAttr "url"
2019-01-27 23:18:10 +03:00
-- 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 ()
)
-- If the type attribute is not set, we try to infer its value based on the url suffix
(,) <$> getPackageSpecAttr "type" <*> getPackageSpecAttr "url" >>= \case
-- If an url type is set, we'll use it
(Just _, _) -> pure ()
-- We need an url to infer a url type
(_, Nothing) -> pure ()
(Nothing, Just (Aeson.String url)) -> do
let urlType = if "tar.gz" `T.isSuffixOf` url
then "tarball"
else "file"
setPackageSpecAttr "type" (Aeson.String $ T.pack urlType)
-- If the JSON value is not a string, we ignore it
(_, _) -> pure ()
2019-01-27 23:18:10 +03:00
-- Updates the sha256 based on the URL contents
2019-02-22 20:54:51 +03:00
(,) <$> getPackageSpecAttr "url" <*> getPackageSpecAttr "sha256" >>= \case
-- If no URL is set, we simply can't prefetch
(Nothing, _) -> pure ()
-- If an URL is set and no sha is set, /do/ update
(Just url, Nothing) -> prefetch url
-- If both the URL and sha are set, update only if the url has changed
(Just url, Just{}) -> when (Just url /= originalUrl) (prefetch url)
where
prefetch :: Aeson.Value -> StateT PackageSpec IO ()
prefetch = \case
2019-01-28 23:29:35 +03:00
Aeson.String (T.unpack -> url) -> do
unpack <- getPackageSpecAttr "type" <&> \case
-- Do not unpack if the url type is 'file'
Just (Aeson.String urlType) -> not $ T.unpack urlType == "file"
_ -> True
sha256 <- liftIO $ nixPrefetchURL unpack 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_
[ (pathNixSourcesJson, initNixSourcesJsonContent)
2019-02-01 17:42:05 +03:00
, (pathNixSourcesNix, initNixSourcesNixContent)
2019-01-28 23:25:09 +03:00
, (pathNixDefaultNix, initNixDefaultNixContent)
2019-02-07 23:12:39 +03:00
, (pathNixPackagesNix, initNixPackagesNixContent)
2019-01-28 23:25:09 +03:00
, (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' ..."
2019-01-31 02:05:35 +03:00
cmdAdd Nothing (PackageName "nmattia/niv", PackageSpec HMap.empty)
2019-01-28 23:25:09 +03:00
putStrLn "Importing 'nixpkgs' ..."
cmdAdd
2019-01-31 02:05:35 +03:00
(Just (PackageName "nixpkgs"))
2019-01-28 23:25:09 +03:00
( PackageName "NixOS/nixpkgs-channels"
, PackageSpec (HMap.singleton "branch" "nixos-18.09"))
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-31 02:05:35 +03:00
Opts.info ((cmdAdd <$> optName <*> parsePackage) <**> Opts.helper) $
2019-01-29 00:32:36 +03:00
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' <>
2019-01-31 02:05:35 +03:00
Opts.metavar "NAME" <>
Opts.help "Set the package name to <NAME>"
2019-01-27 23:18:10 +03:00
)
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-channels -n nixpkgs -b nixos-18.09" Opts.<$$>
2019-01-29 00:32:36 +03:00
" 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-31 02:05:35 +03:00
cmdAdd :: Maybe PackageName -> (PackageName, PackageSpec) -> IO ()
cmdAdd mPackageName (PackageName str, spec) = 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
sources <- unSources <$> getSources
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
when (HMap.member packageName' sources) $
2019-01-28 23:51:10 +03:00
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
putStrLn $ "Writing new sources file"
setSources $ Sources $
HMap.insert packageName' spec'' sources
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 sources file"
2019-01-23 23:55:26 +03:00
sources <- unSources <$> getSources
2019-01-23 23:55:26 +03:00
forWithKeyM_ sources $ \key (PackageSpec spec) -> do
2019-01-23 23:55:26 +03:00
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
sources <- unSources <$> getSources
2019-01-23 23:55:26 +03:00
packageSpec' <- case HMap.lookup packageName sources of
2019-01-27 23:18:10 +03:00
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
setSources $ Sources $
HMap.insert packageName packageSpec' sources
2019-01-23 23:55:26 +03:00
2019-01-27 23:18:10 +03:00
Nothing -> do
sources <- unSources <$> getSources
2019-01-23 23:55:26 +03:00
sources' <- forWithKeyM sources $
2019-01-27 23:18:10 +03:00
\packageName packageSpec -> do
putStrLn $ "Package: " <> unPackageName packageName
2019-01-31 22:42:39 +03:00
updatePackageSpec =<< completePackageSpec packageSpec
2019-01-23 23:55:26 +03:00
setSources $ Sources sources'
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-02-08 21:07:05 +03:00
((cmdDrop <$> parsePackageName <*> parseDropAttributes) <**>
Opts.helper) $
2019-01-29 00:32:36 +03:00
mconcat desc
where
desc =
[ Opts.fullDesc
, Opts.progDesc "Drop dependency"
, Opts.headerDoc $ Just $
"Examples:" Opts.<$$>
"" Opts.<$$>
2019-02-08 21:07:05 +03:00
" niv drop jq" Opts.<$$>
" niv drop my-package version"
2019-01-29 00:32:36 +03:00
]
2019-02-08 21:07:05 +03:00
parseDropAttributes :: Opts.Parser [T.Text]
parseDropAttributes = many $
Opts.argument Opts.str (Opts.metavar "ATTRIBUTE")
2019-01-27 23:18:10 +03:00
2019-02-08 21:07:05 +03:00
cmdDrop :: PackageName -> [T.Text] -> IO ()
cmdDrop packageName = \case
[] -> do
2019-01-27 23:18:10 +03:00
putStrLn $ "Dropping package: " <> unPackageName packageName
sources <- unSources <$> getSources
2019-01-27 23:18:10 +03:00
when (not $ HMap.member packageName sources) $
2019-01-28 23:51:10 +03:00
abortCannotDropNoSuchPackage packageName
2019-01-18 01:00:48 +03:00
setSources $ Sources $
HMap.delete packageName sources
2019-02-08 21:07:05 +03:00
attrs -> do
putStrLn $ "Dropping attributes :" <>
(T.unpack (T.intercalate " " attrs))
putStrLn $ "In package: " <> unPackageName packageName
sources <- unSources <$> getSources
packageSpec <- case HMap.lookup packageName sources of
Nothing ->
abortCannotAttributesDropNoSuchPackage packageName
Just (PackageSpec packageSpec) -> pure $ PackageSpec $
HMap.mapMaybeWithKey
(\k v -> if k `elem` attrs then Nothing else Just v) packageSpec
setSources $ Sources $
HMap.insert packageName packageSpec sources
2019-01-27 23:18:10 +03:00
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-02-07 16:15:56 +03:00
encodeFile fp = L.writeFile fp . AesonPretty.encodePretty
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
nixPrefetchURL :: Bool -> String -> IO String
nixPrefetchURL unpack url =
lines <$> readProcess "nix-prefetch-url" args "" >>=
2019-01-29 00:32:36 +03:00
\case
(l:_) -> pure l
_ -> abortNixPrefetchExpectedOutput
where args = if unpack then ["--unpack", url] else [url]
2019-01-29 00:32:36 +03:00
2019-01-28 23:25:09 +03:00
-------------------------------------------------------------------------------
-- Files and their content
-------------------------------------------------------------------------------
2019-02-01 17:42:05 +03:00
-- | @nix/sources.nix@
pathNixSourcesNix :: FilePath
pathNixSourcesNix = "nix" </> "sources.nix"
2019-01-28 23:25:09 +03:00
-- | Glue code between nix and sources.json
2019-02-01 17:42:05 +03:00
initNixSourcesNixContent :: String
initNixSourcesNixContent = [s|
2019-01-28 23:25:09 +03:00
# A record, from name to path, of the third-party packages
with rec
2019-02-01 17:42:05 +03:00
{
2019-02-22 13:38:26 +03:00
sources = builtins.fromJSON (builtins.readFile ./sources.json);
2019-02-01 17:42:05 +03:00
2019-02-22 13:38:26 +03:00
# fetchTarball version that is compatible between all the sources of Nix
2019-01-28 23:25:09 +03:00
fetchTarball =
2019-02-01 17:42:05 +03:00
{ url, sha256 }:
if builtins.lessThan builtins.nixVersion "1.12" then
builtins.fetchTarball { inherit url; }
2019-01-28 23:25:09 +03:00
else
2019-02-01 17:42:05 +03:00
builtins.fetchTarball { inherit url sha256; };
2019-02-22 13:38:26 +03:00
mapAttrs = builtins.mapAttrs or
(f: set: with builtins;
listToAttrs (map (attr: { name = attr; value = f attr set.${attr}; }) (attrNames set)));
2019-02-01 17:42:05 +03:00
getFetcher = spec:
let fetcherName =
if builtins.hasAttr "type" spec
then builtins.getAttr "type" spec
else "tarball";
in builtins.getAttr fetcherName {
"tarball" = builtins.fetchTarball;
"file" = builtins.fetchurl;
};
};
2019-02-01 17:42:05 +03:00
# NOTE: spec must _not_ have an "outPath" attribute
2019-02-22 13:38:26 +03:00
mapAttrs (_: spec:
2019-02-01 17:42:05 +03:00
if builtins.hasAttr "outPath" spec
then abort
2019-02-22 13:38:26 +03:00
"The values in sources.json should not have an 'outPath' attribute"
2019-02-01 17:42:05 +03:00
else
if builtins.hasAttr "url" spec && builtins.hasAttr "sha256" spec
then
spec //
{ outPath = getFetcher spec { inherit (spec) url sha256; } ; }
2019-02-01 17:42:05 +03:00
else spec
2019-02-22 13:38:26 +03:00
) sources
2019-01-28 23:25:09 +03:00
|]
-- | @nix/default.nix@
pathNixDefaultNix :: FilePath
pathNixDefaultNix = "nix" </> "default.nix"
-- | File importing @nixpkgs@, setting up overlays, etc
initNixDefaultNixContent :: String
initNixDefaultNixContent = [s|
2019-02-07 23:12:39 +03:00
{ sources ? import ./sources.nix }:
with
{ overlay = _: pkgs:
{ inherit (import sources.niv {}) niv;
packages = pkgs.callPackages ./packages.nix {};
};
};
2019-02-01 17:42:05 +03:00
import sources.nixpkgs
2019-02-07 23:12:39 +03:00
{ overlays = [ overlay ] ; config = {}; }
2019-02-01 17:42:05 +03:00
|]
2019-02-07 23:12:39 +03:00
-- | @nix/packages.nix@
pathNixPackagesNix :: FilePath
pathNixPackagesNix = "nix" </> "packages.nix"
2019-02-01 17:42:05 +03:00
2019-02-07 23:12:39 +03:00
-- | File with packages
initNixPackagesNixContent :: String
initNixPackagesNixContent = [s|
{ writeScriptBin
}:
{ foo = writeScriptBin "foo" "echo foo" ; }
2019-01-28 23:25:09 +03:00
|]
-- | @default.nix@
pathDefaultNix :: FilePath
pathDefaultNix = "default.nix"
-- | Top level @default.nix@
initDefaultNixContent :: String
initDefaultNixContent = [s|
2019-02-07 23:12:39 +03:00
let pkgs = import ./nix {}; in pkgs.packages
2019-01-28 23:25:09 +03:00
|]
-- | @shell.nix@
pathShellNix :: FilePath
pathShellNix = "shell.nix"
-- | Simple shell that loads @niv@
initShellNixContent :: String
initShellNixContent = [s|
2019-02-03 16:07:42 +03:00
with { pkgs = import ./nix {}; };
pkgs.mkShell
2019-01-28 23:25:09 +03:00
{ buildInputs = [ pkgs.niv ];
}
|]
-- | @nix/sources.json"
pathNixSourcesJson :: FilePath
pathNixSourcesJson = "nix" </> "sources.json"
2019-01-28 23:25:09 +03:00
-- | Empty JSON map
initNixSourcesJsonContent :: String
initNixSourcesJsonContent = "{}"
2019-01-28 23:51:10 +03:00
-------------------------------------------------------------------------------
-- Abort
-------------------------------------------------------------------------------
abortSourcesIsntAMap :: IO a
abortSourcesIsntAMap = abort $ unlines [ line1, line2 ]
2019-01-28 23:51:10 +03:00
where
line1 = "Cannot use " <> pathNixSourcesJson
2019-01-28 23:51:10 +03:00
line2 = [s|
The sources file should be a JSON map from package name to package
2019-01-28 23:51:10 +03:00
specification, e.g.:
{ ... }
|]
abortAttributeIsntAMap :: IO a
abortAttributeIsntAMap = abort $ unlines [ line1, line2 ]
where
line1 = "Cannot use " <> pathNixSourcesJson
2019-01-28 23:51:10 +03:00
line2 = [s|
The package specifications in the sources file should be JSON maps from
2019-01-28 23:51:10 +03:00
attribute name to attribute value, e.g.:
{ "nixpkgs": { "foo": "bar" } }
|]
abortSourcesIsntJSON :: IO a
abortSourcesIsntJSON = abort $ unlines [ line1, line2 ]
2019-01-28 23:51:10 +03:00
where
line1 = "Cannot use " <> pathNixSourcesJson
line2 = "The sources file should be JSON."
2019-01-28 23:51:10 +03:00
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."
]
2019-02-08 21:07:05 +03:00
abortCannotAttributesDropNoSuchPackage :: PackageName -> IO a
abortCannotAttributesDropNoSuchPackage (PackageName n) = abort $ unlines
[ "Cannot drop attributes of package " <> n <> "."
, "The package doesn't exist."
]
2019-01-28 23:51:10 +03:00
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.
|]