niv 0.2.1 → 0.2.2
raw patch · 10 files changed
+455/−271 lines, 10 filesdep +pureMD5PVP: major bump suggested
API removals or changes: PVP suggests a major version bump
Dependencies added: pureMD5
API changes (from Hackage documentation)
- Niv.Cli: PackageName :: Text -> PackageName
- Niv.Cli: PackageSpec :: Object -> PackageSpec
- Niv.Cli: Sources :: HashMap PackageName PackageSpec -> Sources
- Niv.Cli: [unPackageName] :: PackageName -> Text
- Niv.Cli: [unPackageSpec] :: PackageSpec -> Object
- Niv.Cli: [unSources] :: Sources -> HashMap PackageName PackageSpec
- Niv.Cli: abortAttributeIsntAMap :: IO a
- Niv.Cli: abortSourcesDoesntExist :: IO a
- Niv.Cli: abortSourcesIsntJSON :: IO a
- Niv.Cli: attrsToSpec :: Attrs -> PackageSpec
- Niv.Cli: decodeFileStrict :: FromJSON a => FilePath -> IO (Maybe a)
- Niv.Cli: encodeFile :: ToJSON a => FilePath -> a -> IO ()
- Niv.Cli: forWithKeyM :: (Eq k, Hashable k, Monad m) => HashMap k v1 -> (k -> v1 -> m v2) -> m (HashMap k v2)
- Niv.Cli: forWithKeyM_ :: (Eq k, Hashable k, Monad m) => HashMap k v1 -> (k -> v1 -> m ()) -> m ()
- Niv.Cli: getSources :: IO Sources
- Niv.Cli: initNixSourcesJsonContent :: ByteString
- Niv.Cli: initNixSourcesNixContent :: ByteString
- Niv.Cli: instance Data.Aeson.Types.FromJSON.FromJSON Niv.Cli.PackageSpec
- Niv.Cli: instance Data.Aeson.Types.FromJSON.FromJSON Niv.Cli.Sources
- Niv.Cli: instance Data.Aeson.Types.FromJSON.FromJSONKey Niv.Cli.PackageName
- Niv.Cli: instance Data.Aeson.Types.ToJSON.ToJSON Niv.Cli.PackageSpec
- Niv.Cli: instance Data.Aeson.Types.ToJSON.ToJSON Niv.Cli.Sources
- Niv.Cli: instance Data.Aeson.Types.ToJSON.ToJSONKey Niv.Cli.PackageName
- Niv.Cli: instance Data.Hashable.Class.Hashable Niv.Cli.PackageName
- Niv.Cli: instance GHC.Base.Monoid Niv.Cli.PackageSpec
- Niv.Cli: instance GHC.Base.Semigroup Niv.Cli.PackageSpec
- Niv.Cli: instance GHC.Classes.Eq Niv.Cli.PackageName
- Niv.Cli: instance GHC.Show.Show Niv.Cli.PackageName
- Niv.Cli: instance GHC.Show.Show Niv.Cli.PackageSpec
- Niv.Cli: mapWithKeyM :: (Eq k, Hashable k, Monad m) => (k -> v1 -> m v2) -> HashMap k v1 -> m (HashMap k v2)
- Niv.Cli: mapWithKeyM_ :: (Eq k, Hashable k, Monad m) => (k -> v1 -> m ()) -> HashMap k v1 -> m ()
- Niv.Cli: newtype PackageName
- Niv.Cli: newtype PackageSpec
- Niv.Cli: newtype Sources
- Niv.Cli: pathNixSourcesJson :: FilePath
- Niv.Cli: pathNixSourcesNix :: FilePath
- Niv.Cli: setSources :: Sources -> IO ()
- Niv.Cli: warnIfOutdated :: IO ()
+ Data.Aeson.Extended: encodeFilePretty :: ToJSON a => FilePath -> a -> IO ()
+ Data.HashMap.Strict.Extended: forWithKeyM :: (Eq k, Hashable k, Monad m) => HashMap k v1 -> (k -> v1 -> m v2) -> m (HashMap k v2)
+ Data.HashMap.Strict.Extended: forWithKeyM_ :: (Eq k, Hashable k, Monad m) => HashMap k v1 -> (k -> v1 -> m ()) -> m ()
+ Data.HashMap.Strict.Extended: mapWithKeyM :: (Eq k, Hashable k, Monad m) => (k -> v1 -> m v2) -> HashMap k v1 -> m (HashMap k v2)
+ Data.HashMap.Strict.Extended: mapWithKeyM_ :: (Eq k, Hashable k, Monad m) => (k -> v1 -> m ()) -> HashMap k v1 -> m ()
+ Niv.Cli: githubUpdate' :: Update () ()
+ Niv.Cli: showPackage :: PackageName -> PackageSpec -> IO ()
+ Niv.Logger: blue :: String -> String
+ Niv.Logger: tblue :: Text -> Text
+ Niv.Logger: tyellow :: Text -> Text
+ Niv.Logger: yellow :: String -> String
+ Niv.Sources: PackageName :: Text -> PackageName
+ Niv.Sources: PackageSpec :: Object -> PackageSpec
+ Niv.Sources: SourceIsntJSON :: SourcesError
+ Niv.Sources: Sources :: HashMap PackageName PackageSpec -> Sources
+ Niv.Sources: SourcesDoesntExist :: SourcesError
+ Niv.Sources: SpecIsntAMap :: SourcesError
+ Niv.Sources: V1 :: SourcesNixVersion
+ Niv.Sources: V10 :: SourcesNixVersion
+ Niv.Sources: V2 :: SourcesNixVersion
+ Niv.Sources: V3 :: SourcesNixVersion
+ Niv.Sources: V4 :: SourcesNixVersion
+ Niv.Sources: V5 :: SourcesNixVersion
+ Niv.Sources: V6 :: SourcesNixVersion
+ Niv.Sources: V7 :: SourcesNixVersion
+ Niv.Sources: V8 :: SourcesNixVersion
+ Niv.Sources: V9 :: SourcesNixVersion
+ Niv.Sources: [unPackageName] :: PackageName -> Text
+ Niv.Sources: [unPackageSpec] :: PackageSpec -> Object
+ Niv.Sources: [unSources] :: Sources -> HashMap PackageName PackageSpec
+ Niv.Sources: abortSourcesDoesntExist :: IO a
+ Niv.Sources: abortSourcesIsntJSON :: IO a
+ Niv.Sources: abortSpecIsntAMap :: IO a
+ Niv.Sources: attrsToSpec :: Attrs -> PackageSpec
+ Niv.Sources: data SourcesError
+ Niv.Sources: data SourcesNixVersion
+ Niv.Sources: getSources :: IO Sources
+ Niv.Sources: getSourcesEither :: IO (Either SourcesError Sources)
+ Niv.Sources: initNixSourcesJsonContent :: ByteString
+ Niv.Sources: initNixSourcesNixContent :: ByteString
+ Niv.Sources: instance Data.Aeson.Types.FromJSON.FromJSON Niv.Sources.PackageSpec
+ Niv.Sources: instance Data.Aeson.Types.FromJSON.FromJSON Niv.Sources.Sources
+ Niv.Sources: instance Data.Aeson.Types.FromJSON.FromJSONKey Niv.Sources.PackageName
+ Niv.Sources: instance Data.Aeson.Types.ToJSON.ToJSON Niv.Sources.PackageSpec
+ Niv.Sources: instance Data.Aeson.Types.ToJSON.ToJSON Niv.Sources.Sources
+ Niv.Sources: instance Data.Aeson.Types.ToJSON.ToJSONKey Niv.Sources.PackageName
+ Niv.Sources: instance Data.Hashable.Class.Hashable Niv.Sources.PackageName
+ Niv.Sources: instance GHC.Base.Monoid Niv.Sources.PackageSpec
+ Niv.Sources: instance GHC.Base.Semigroup Niv.Sources.PackageSpec
+ Niv.Sources: instance GHC.Classes.Eq Niv.Sources.PackageName
+ Niv.Sources: instance GHC.Classes.Eq Niv.Sources.SourcesNixVersion
+ Niv.Sources: instance GHC.Enum.Bounded Niv.Sources.SourcesNixVersion
+ Niv.Sources: instance GHC.Enum.Enum Niv.Sources.SourcesNixVersion
+ Niv.Sources: instance GHC.Show.Show Niv.Sources.PackageName
+ Niv.Sources: instance GHC.Show.Show Niv.Sources.PackageSpec
+ Niv.Sources: latestVersionMD5 :: Text
+ Niv.Sources: md5ToSourcesVersion :: Text -> Maybe SourcesNixVersion
+ Niv.Sources: newtype PackageName
+ Niv.Sources: newtype PackageSpec
+ Niv.Sources: newtype Sources
+ Niv.Sources: pathNixSourcesJson :: FilePath
+ Niv.Sources: pathNixSourcesNix :: FilePath
+ Niv.Sources: setSources :: Sources -> IO ()
+ Niv.Sources: sourcesNixMD5 :: IO Text
+ Niv.Sources: sourcesVersionToMD5 :: SourcesNixVersion -> Text
+ Niv.Sources: sourcesVersionToText :: SourcesNixVersion -> Text
+ Niv.Sources: warnIfOutdated :: IO ()
+ Niv.Sources.Test: test_shippedSourcesNixIsLatest :: IO ()
- Niv.Cli: cmdAdd :: Maybe PackageName -> (PackageName, PackageSpec) -> IO ()
+ Niv.Cli: cmdAdd :: Update () a -> PackageName -> Attrs -> IO ()
Files
- README.md +5/−5
- niv.cabal +9/−2
- nix/sources.nix +53/−50
- src/Data/Aeson/Extended.hs +17/−0
- src/Data/HashMap/Strict/Extended.hs +40/−0
- src/Niv/Cli.hs +80/−214
- src/Niv/Logger.hs +18/−0
- src/Niv/Sources.hs +216/−0
- src/Niv/Sources/Test.hs +13/−0
- src/Niv/Test.hs +4/−0
README.md view
@@ -199,7 +199,7 @@ ``` niv - dependency manager for Nix projects -version: 0.2.1+version: 0.2.2 Usage: niv COMMAND @@ -251,10 +251,10 @@ ``` Examples:-- niv update- niv update nixpkgs- niv update my-package -v beta-0.2+ + niv update # update all packages+ niv update nixpkgs # update nixpkgs+ niv update my-package -v beta-0.2 # update my-package to version "beta-0.2" Usage: niv update [PACKAGE] ([-a|--attribute KEY=VAL] | [-b|--branch BRANCH] | [-o|--owner OWNER] | [-r|--repo REPO] | [-v|--version VERSION]
niv.cabal view
@@ -4,10 +4,10 @@ -- -- see: https://github.com/sol/hpack ----- hash: 7cabb5fdfc03b35748a6e8348284d303786488a08d6819852d4df7d428791e6e+-- hash: 4ae2ca2b71f11737dafa898e811c9e1a8f33a82bb9c5365bff2ff9e9d89dab93 name: niv-version: 0.2.1+version: 0.2.2 synopsis: Easy dependency management for Nix projects description: Easy dependency management for Nix projects. category: Development@@ -30,10 +30,14 @@ library exposed-modules:+ Data.Aeson.Extended+ Data.HashMap.Strict.Extended Niv.Cli Niv.GitHub Niv.GitHub.Test Niv.Logger+ Niv.Sources+ Niv.Sources.Test Niv.Test Niv.Update Niv.Update.Test@@ -57,6 +61,7 @@ , optparse-applicative , process , profunctors+ , pureMD5 , string-qq , tasty , tasty-hunit@@ -89,6 +94,7 @@ , optparse-applicative , process , profunctors+ , pureMD5 , string-qq , text , unliftio@@ -119,6 +125,7 @@ , optparse-applicative , process , profunctors+ , pureMD5 , string-qq , tasty , text
nix/sources.nix view
@@ -6,42 +6,40 @@ pkgs = if hasNixpkgsPath then- if hasThisAsNixpkgsPath- then import (builtins_fetchTarball { inherit (sources_nixpkgs) url sha256; }) {}- else import <nixpkgs> {}+ if hasThisAsNixpkgsPath+ then import (builtins_fetchTarball { inherit (sources_nixpkgs) url sha256; }) {}+ else import <nixpkgs> {} else- import (builtins_fetchTarball { inherit (sources_nixpkgs) url sha256; }) {};+ import (builtins_fetchTarball { inherit (sources_nixpkgs) url sha256; }) {}; sources_nixpkgs = if builtins.hasAttr "nixpkgs" sources then sources.nixpkgs else abort- ''+ '' Please specify either <nixpkgs> (through -I or NIX_PATH=nixpkgs=...) or add a package called "nixpkgs" to your sources.json.- '';+ ''; # fetchTarball version that is compatible between all the versions of Nix- builtins_fetchTarball =- { url, sha256 }@attrs:- let- inherit (builtins) lessThan nixVersion fetchTarball;- in- if lessThan nixVersion "1.12" then- fetchTarball { inherit url; }- else- fetchTarball attrs;+ builtins_fetchTarball = { url, sha256 }@attrs:+ let+ inherit (builtins) lessThan nixVersion fetchTarball;+ in+ if lessThan nixVersion "1.12" then+ fetchTarball { inherit url; }+ else+ fetchTarball attrs; # fetchurl version that is compatible between all the versions of Nix- builtins_fetchurl =- { url, sha256 }@attrs:- let- inherit (builtins) lessThan nixVersion fetchurl;- in- if lessThan nixVersion "1.12" then- fetchurl { inherit url; }- else- fetchurl attrs;+ builtins_fetchurl = { url, sha256 }@attrs:+ let+ inherit (builtins) lessThan nixVersion fetchurl;+ in+ if lessThan nixVersion "1.12" then+ fetchurl { inherit url; }+ else+ fetchurl attrs; # A wrapper around pkgs.fetchzip that has inspectable arguments, # annoyingly this means we have to specify them@@ -57,37 +55,42 @@ sources = builtins.fromJSON (builtins.readFile ./sources.json); - mapAttrs = builtins.mapAttrs or- (f: set: with builtins;- listToAttrs (map (attr: { name = attr; value = f attr set.${attr}; }) (attrNames set)));+ mapAttrs = builtins.mapAttrs or (+ f: set: with builtins;+ listToAttrs (map (attr: { name = attr; value = f attr set.${attr}; }) (attrNames set))+ ); # borrowed from nixpkgs functionArgs = f: f.__functionArgs or (builtins.functionArgs f); callFunctionWith = autoArgs: f: args:- let auto = builtins.intersectAttrs (functionArgs f) autoArgs;- in f (auto // args);+ let+ auto = builtins.intersectAttrs (functionArgs f) autoArgs;+ in+ f (auto // args); getFetcher = spec:- let fetcherName =- if builtins.hasAttr "type" spec- then builtins.getAttr "type" spec- else "builtin-tarball";- in builtins.getAttr fetcherName {- "tarball" = fetchzip;- "builtin-tarball" = builtins_fetchTarball;- "file" = fetchurl;- "builtin-url" = builtins_fetchurl;- };+ let+ fetcherName =+ if builtins.hasAttr "type" spec+ then builtins.getAttr "type" spec+ else "builtin-tarball";+ in+ builtins.getAttr fetcherName {+ "tarball" = fetchzip;+ "builtin-tarball" = builtins_fetchTarball;+ "file" = fetchurl;+ "builtin-url" = builtins_fetchurl;+ }; }; # NOTE: spec must _not_ have an "outPath" attribute-mapAttrs (_: spec:- if builtins.hasAttr "outPath" spec- then abort- "The values in sources.json should not have an 'outPath' attribute"- else- if builtins.hasAttr "url" spec && builtins.hasAttr "sha256" spec- then- spec //- { outPath = callFunctionWith spec (getFetcher spec) { }; }- else spec- ) sources+mapAttrs (+ _: spec:+ if builtins.hasAttr "outPath" spec+ then abort+ "The values in sources.json should not have an 'outPath' attribute"+ else+ if builtins.hasAttr "url" spec && builtins.hasAttr "sha256" spec+ then+ spec // { outPath = callFunctionWith spec (getFetcher spec) {}; }+ else spec+) sources
+ src/Data/Aeson/Extended.hs view
@@ -0,0 +1,17 @@+module Data.Aeson.Extended where++import Data.Aeson (ToJSON)+import qualified Data.Aeson.Encode.Pretty as AesonPretty+import qualified Data.ByteString.Lazy as BL++--- Aeson++-- | Efficiently prettify and serialize a JSON value as a lazy 'BL.ByteString'+-- and write it to a file.+encodeFilePretty :: (ToJSON a) => FilePath -> a -> IO ()+encodeFilePretty fp = BL.writeFile fp . AesonPretty.encodePretty' config+ where+ config = AesonPretty.defConfig {+ AesonPretty.confTrailingNewline = True,+ AesonPretty.confCompare = compare+ }
+ src/Data/HashMap/Strict/Extended.hs view
@@ -0,0 +1,40 @@++module Data.HashMap.Strict.Extended where++import Control.Monad+import Data.Hashable (Hashable)+import qualified Data.HashMap.Strict as HMS++--- HashMap++forWithKeyM+ :: (Eq k, Hashable k, Monad m)+ => HMS.HashMap k v1+ -> (k -> v1 -> m v2)+ -> m (HMS.HashMap k v2)+forWithKeyM = flip mapWithKeyM++forWithKeyM_+ :: (Eq k, Hashable k, Monad m)+ => HMS.HashMap k v1+ -> (k -> v1 -> m ())+ -> m ()+forWithKeyM_ = flip mapWithKeyM_++mapWithKeyM+ :: (Eq k, Hashable k, Monad m)+ => (k -> v1 -> m v2)+ -> HMS.HashMap k v1+ -> m (HMS.HashMap k v2)+mapWithKeyM f m = do+ fmap mconcat $ forM (HMS.toList m) $ \(k, v) ->+ HMS.singleton k <$> f k v++mapWithKeyM_+ :: (Eq k, Hashable k, Monad m)+ => (k -> v1 -> m ())+ -> HMS.HashMap k v1+ -> m ()+mapWithKeyM_ f m = do+ forM_ (HMS.toList m) $ \(k, v) ->+ HMS.singleton k <$> f k v
src/Niv/Cli.hs view
@@ -1,5 +1,4 @@ {-# LANGUAGE DerivingStrategies #-}-{-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-}@@ -11,28 +10,27 @@ import Control.Applicative import Control.Monad-import Data.Aeson (FromJSON, FromJSONKey, ToJSON, ToJSONKey, (.=))+import Data.Aeson ((.=)) import Data.Char (isSpace)-import Data.FileEmbed (embedFile)+import Data.Functor+import Data.HashMap.Strict.Extended import Data.Hashable (Hashable)-import Data.Maybe (fromMaybe) import Data.String.QQ (s)-import Niv.Logger+import Data.Version (showVersion) import Niv.GitHub+import Niv.Logger+import Niv.Sources import Niv.Update+import System.Environment (getArgs) import System.Exit (ExitCode(ExitSuccess))-import System.FilePath ((</>), takeDirectory)+import System.FilePath (takeDirectory) import System.Process (readProcessWithExitCode) import UnliftIO-import Data.Version (showVersion) import qualified Data.Aeson as Aeson-import qualified Data.Aeson.Encode.Pretty as AesonPretty import qualified Data.ByteString as B import qualified Data.ByteString.Char8 as B8-import qualified Data.ByteString.Lazy as L import qualified Data.HashMap.Strict as HMS import qualified Data.Text as T-import qualified Data.Text.IO as T import qualified Options.Applicative as Opts import qualified Options.Applicative.Help.Pretty as Opts import qualified System.Directory as Dir@@ -41,9 +39,14 @@ import Paths_niv (version) cli :: IO ()-cli = join $ Opts.execParser opts+cli = join $+ execParserPure' Opts.defaultPrefs opts <$> getArgs+ >>= Opts.handleParseResult where- opts = Opts.info (parseCommand <**> Opts.helper) $ mconcat desc+ execParserPure' pprefs pinfo [] = Opts.Failure $+ Opts.parserFailure pprefs pinfo Opts.ShowHelpText mempty+ execParserPure' pprefs pinfo args = Opts.execParserPure pprefs pinfo args+ opts = Opts.info (parseCommand <**> Opts.helper ) $ mconcat desc desc = [ Opts.fullDesc , Opts.headerDoc $ Just $@@ -61,46 +64,10 @@ Opts.command "modify" parseCmdModify <> Opts.command "drop" parseCmdDrop ) -newtype Sources = Sources- { unSources :: HMS.HashMap PackageName PackageSpec }- deriving newtype (FromJSON, ToJSON)--getSources :: IO Sources-getSources = do- exists <- Dir.doesFileExist pathNixSourcesJson- unless exists abortSourcesDoesntExist-- warnIfOutdated- -- TODO: if doesn't exist: run niv init- say $ "Reading sources file"- decodeFileStrict pathNixSourcesJson >>= \case- Just (Aeson.Object obj) ->- fmap (Sources . mconcat) $- forM (HMS.toList obj) $ \(k, v) ->- case v of- Aeson.Object v' ->- pure $ HMS.singleton (PackageName k) (PackageSpec v')- _ -> abortAttributeIsntAMap- Just _ -> abortSourcesIsntAMap- Nothing -> abortSourcesIsntJSON--setSources :: Sources -> IO ()-setSources sources = encodeFile pathNixSourcesJson sources--newtype PackageName = PackageName { unPackageName :: T.Text }- deriving newtype (Eq, Hashable, FromJSONKey, ToJSONKey, Show)- parsePackageName :: Opts.Parser PackageName parsePackageName = PackageName <$> Opts.argument Opts.str (Opts.metavar "PACKAGE") -newtype PackageSpec = PackageSpec { unPackageSpec :: Aeson.Object }- deriving newtype (FromJSON, ToJSON, Show, Semigroup, Monoid)---- | Simply discards the 'Freedom'-attrsToSpec :: Attrs -> PackageSpec-attrsToSpec = PackageSpec . fmap snd- parsePackageSpec :: Opts.Parser PackageSpec parsePackageSpec = (PackageSpec . HMS.fromList . fmap fixupAttributes) <$>@@ -193,12 +160,20 @@ createFile path initNixSourcesJsonContent -- Imports @niv@ and @nixpkgs@ (19.03) say "Importing 'niv' ..."- cmdAdd Nothing (PackageName "nmattia/niv", PackageSpec HMS.empty)+ cmdAdd githubUpdate' (PackageName "niv")+ (specToFreeAttrs $ PackageSpec $ HMS.fromList+ [ "owner" .= ("nmattia" :: T.Text)+ , "repo" .= ("niv" :: T.Text)+ ]+ ) say "Importing 'nixpkgs' ..."- cmdAdd- (Just (PackageName "nixpkgs"))- ( PackageName "NixOS/nixpkgs-channels"- , PackageSpec (HMS.singleton "branch" "nixos-19.03"))+ cmdAdd githubUpdate' (PackageName "nixpkgs")+ (specToFreeAttrs $ PackageSpec $ HMS.fromList+ [ "owner" .= ("NixOS" :: T.Text)+ , "repo" .= ("nixpkgs-channels" :: T.Text)+ , "branch" .= ("nixos-19.03" :: T.Text)+ ]+ ) , \path _content -> dontCreateFile path) ] $ \(path, onCreate, onUpdate) -> do exists <- Dir.doesFileExist path@@ -219,16 +194,37 @@ parseCmdAdd :: Opts.ParserInfo (IO ()) parseCmdAdd =- Opts.info ((cmdAdd <$> optName <*> parsePackage) <**> Opts.helper) $+ Opts.info+ ((uncurry (cmdAdd githubUpdate') <$> parseArgs) <**> Opts.helper) $ mconcat desc where- optName :: Opts.Parser (Maybe PackageName)- optName = Opts.optional $ PackageName <$> Opts.strOption+ parseArgs :: Opts.Parser (PackageName, Attrs)+ parseArgs = collapse <$> parseNameAndGHShortcut <*> parsePackageSpec+ parseNameAndGHShortcut = (,) <$> optName <*> parseGitHubShortcut+ -- collaspe a "name or shortcut" with package spec+ collapse nameAndSpec pspec = (pname, specToLockedAttrs $ pspec <> repoAndOwner)+ where+ (pname, repoAndOwner) = case nameAndSpec of+ (Just pname', (_, spec)) -> (pname', PackageSpec spec)+ (Nothing, (pname', spec)) -> (pname', PackageSpec spec)+ optName = Opts.optional $ PackageName <$> Opts.strOption ( Opts.long "name" <> Opts.short 'n' <> Opts.metavar "NAME" <> Opts.help "Set the package name to <NAME>" )++ -- parse a github shortcut of the form "owner/repo"+ parseGitHubShortcut = Opts.strArgument (Opts.metavar "PACKAGE") <&>+ -- parses a string "owner/repo" into package name (repo) and spec (owner ++ -- repo)+ \(T.pack -> str) ->+ case T.span (/= '/') str of+ (owner@(T.null -> False)+ , T.uncons -> Just ('/', repo@(T.null -> False))) ->+ ( PackageName repo+ , HMS.fromList [ "owner" .= owner, "repo" .= repo ])+ _ -> (PackageName str, HMS.empty) desc = [ Opts.fullDesc , Opts.progDesc "Add dependency"@@ -240,38 +236,22 @@ " niv add my-package -v alpha-0.1 -t http://example.com/archive/<version>.zip" ] -cmdAdd :: Maybe PackageName -> (PackageName, PackageSpec) -> IO ()-cmdAdd mPackageName (PackageName str, cliSpec) =- job ("Adding package " <> T.unpack str) $ do-- -- Figures out the owner and repo- let (packageName, defaultSpec) = case T.span (/= '/') str of- ( owner@(T.null -> False)- , T.uncons -> Just ('/', repo@(T.null -> False))) -> do- (PackageName repo, HMS.fromList [ "owner" .= owner, "repo" .= repo ])- _ -> (PackageName str, HMS.empty)-+cmdAdd :: Update () a -> PackageName -> Attrs -> IO ()+cmdAdd updateFunc packageName attrs = do+ job ("Adding package " <> T.unpack (unPackageName packageName)) $ do sources <- unSources <$> getSources - let packageName' = fromMaybe packageName mPackageName-- when (HMS.member packageName' sources) $- abortCannotAddPackageExists packageName'-- let defaultSpec' = PackageSpec $ defaultSpec-- let initialSpec = specToLockedAttrs cliSpec <> specToFreeAttrs defaultSpec'+ when (HMS.member packageName sources) $+ abortCannotAddPackageExists packageName - eFinalSpec <- fmap attrsToSpec <$> tryEvalUpdate- initialSpec- (githubUpdate nixPrefetchURL githubLatestRev githubRepo)+ eFinalSpec <- fmap attrsToSpec <$> tryEvalUpdate attrs updateFunc case eFinalSpec of- Left e -> abortUpdateFailed [(packageName', e)]+ Left e -> abortUpdateFailed [(packageName, e)] Right finalSpec -> do say $ "Writing new sources file" setSources $ Sources $- HMS.insert packageName' finalSpec sources+ HMS.insert packageName finalSpec sources ------------------------------------------------------------------------------- -- SHOW@@ -287,32 +267,26 @@ cmdShow :: Maybe PackageName -> IO () cmdShow = \case Just packageName -> do- tsay $ "Showing package " <> unPackageName packageName- sources <- unSources <$> getSources case HMS.lookup packageName sources of- Just (PackageSpec spec) -> do- forM_ (HMS.toList spec) $ \(attrName, attrValValue) -> do- let attrValue = case attrValValue of- Aeson.String str -> str- _ -> "<barabajagal>"- tsay $ " " <> attrName <> ": " <> attrValue+ Just pspec -> showPackage packageName pspec Nothing -> abortCannotShowNoSuchPackage packageName Nothing -> do- say $ "Showing sources file"- sources <- unSources <$> getSources+ forWithKeyM_ sources $ showPackage - forWithKeyM_ sources $ \key (PackageSpec spec) -> do- tsay $ "Showing " <> tbold (unPackageName key)- forM_ (HMS.toList spec) $ \(attrName, attrValValue) -> do- let attrValue = case attrValValue of- Aeson.String str -> str- _ -> tfaint "<barabajagal>"- tsay $ " " <> attrName <> ": " <> attrValue+showPackage :: PackageName -> PackageSpec -> IO ()+showPackage (PackageName pname) (PackageSpec spec) = do+ tsay $ tbold pname+ forM_ (HMS.toList spec) $ \(attrName, attrValValue) -> do+ let attrValue = case attrValValue of+ Aeson.String str -> str+ _ -> tfaint "<barabajagal>"+ tsay $ " " <> attrName <> ": " <> attrValue + ------------------------------------------------------------------------------- -- UPDATE -------------------------------------------------------------------------------@@ -326,12 +300,14 @@ desc = [ Opts.fullDesc , Opts.progDesc "Update dependencies"- , Opts.headerDoc $ Just $+ , Opts.headerDoc $ Just $ Opts.nest 2 $ "Examples:" Opts.<$$> "" Opts.<$$>- " niv update" Opts.<$$>- " niv update nixpkgs" Opts.<$$>- " niv update my-package -v beta-0.2"+ Opts.vcat+ [ Opts.fill 30 "niv update" Opts.<+> "# update all packages",+ Opts.fill 30 "niv update nixpkgs" Opts.<+> "# update nixpkgs",+ Opts.fill 30 "niv update my-package -v beta-0.2" Opts.<+> "# update my-package to version \"beta-0.2\""+ ] ] specToFreeAttrs :: PackageSpec -> Attrs@@ -340,7 +316,6 @@ specToLockedAttrs :: PackageSpec -> Attrs specToLockedAttrs = fmap (Locked,) . unPackageSpec --- TODO: sexy logging + concurrent updates cmdUpdate :: Maybe (PackageName, PackageSpec) -> IO () cmdUpdate = \case Just (packageName, cliSpec) ->@@ -473,60 +448,6 @@ -- 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 Aeson.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 . AesonPretty.encodePretty' config- where- config = AesonPretty.defConfig { AesonPretty.confTrailingNewline = True, AesonPretty.confCompare = compare }----- HashMap--forWithKeyM- :: (Eq k, Hashable k, Monad m)- => HMS.HashMap k v1- -> (k -> v1 -> m v2)- -> m (HMS.HashMap k v2)-forWithKeyM = flip mapWithKeyM--forWithKeyM_- :: (Eq k, Hashable k, Monad m)- => HMS.HashMap k v1- -> (k -> v1 -> m ())- -> m ()-forWithKeyM_ = flip mapWithKeyM_--mapWithKeyM- :: (Eq k, Hashable k, Monad m)- => (k -> v1 -> m v2)- -> HMS.HashMap k v1- -> m (HMS.HashMap k v2)-mapWithKeyM f m = do- fmap mconcat $ forM (HMS.toList m) $ \(k, v) ->- HMS.singleton k <$> f k v--mapWithKeyM_- :: (Eq k, Hashable k, Monad m)- => (k -> v1 -> m ())- -> HMS.HashMap k v1- -> m ()-mapWithKeyM_ f m = do- forM_ (HMS.toList m) $ \(k, v) ->- HMS.singleton k <$> f k v- nixPrefetchURL :: Bool -> T.Text -> IO T.Text nixPrefetchURL unpack (T.unpack -> url) = do (exitCode, sout, serr) <- runNixPrefetch@@ -558,53 +479,14 @@ _ -> False _ -> False -warnIfOutdated :: IO ()-warnIfOutdated = do- tryAny (B.readFile pathNixSourcesNix) >>= \case- Left e -> T.putStrLn $ T.unlines- [ "Could not read " <> T.pack pathNixSourcesNix- , "Error: " <> tshow e- ]- Right content ->- if shouldUpdateNixSourcesNix content- then- T.putStrLn $ T.unlines- [ "WARNING: " <> T.pack pathNixSourcesNix <> " is out of date."- , "Please run"- , " niv init"- , "or add the following line in the " <> T.pack pathNixSourcesNix <> " file:"- , " # niv: no_update"- ]- else pure ()---- | @nix/sources.nix@-pathNixSourcesNix :: FilePath-pathNixSourcesNix = "nix" </> "sources.nix"---- | Glue code between nix and sources.json-initNixSourcesNixContent :: B.ByteString-initNixSourcesNixContent = $(embedFile "nix/sources.nix")---- | @nix/sources.json"-pathNixSourcesJson :: FilePath-pathNixSourcesJson = "nix" </> "sources.json"---- | Empty JSON map-initNixSourcesJsonContent :: B.ByteString-initNixSourcesJsonContent = "{}"+-- | The IO (real) github update+githubUpdate' :: Update () ()+githubUpdate' = githubUpdate nixPrefetchURL githubLatestRev githubRepo ------------------------------------------------------------------------------- -- Abort ------------------------------------------------------------------------------- -abortSourcesDoesntExist :: IO a-abortSourcesDoesntExist = abort $ T.unlines [ line1, line2 ]- where- line1 = "Cannot use " <> T.pack pathNixSourcesJson- line2 = [s|-The sources file does not exist! You may need to run 'niv init'.-|]- abortSourcesIsntAMap :: IO a abortSourcesIsntAMap = abort $ T.unlines [ line1, line2 ] where@@ -614,22 +496,6 @@ specification, e.g.: { ... } |]--abortAttributeIsntAMap :: IO a-abortAttributeIsntAMap = abort $ T.unlines [ line1, line2 ]- where- line1 = "Cannot use " <> T.pack pathNixSourcesJson- line2 = [s|-The package specifications in the sources file should be JSON maps from-attribute name to attribute value, e.g.:- { "nixpkgs": { "foo": "bar" } }-|]--abortSourcesIsntJSON :: IO a-abortSourcesIsntJSON = abort $ T.unlines [ line1, line2 ]- where- line1 = "Cannot use " <> T.pack pathNixSourcesJson- line2 = "The sources file should be JSON." abortCannotAddPackageExists :: PackageName -> IO a abortCannotAddPackageExists (PackageName n) = abort $ T.unlines
src/Niv/Logger.hs view
@@ -53,6 +53,24 @@ ANSI.setSGRCode [ANSI.SetColor ANSI.Foreground ANSI.Vivid ANSI.Green] <> str <> ANSI.setSGRCode [ANSI.Reset] +yellow :: String -> String+yellow str =+ ANSI.setSGRCode [ANSI.SetConsoleIntensity ANSI.BoldIntensity] <>+ ANSI.setSGRCode [ANSI.SetColor ANSI.Foreground ANSI.Vivid ANSI.Yellow] <>+ str <> ANSI.setSGRCode [ANSI.Reset]++tyellow :: T.Text -> T.Text+tyellow = dimap T.unpack T.pack yellow++blue :: String -> String+blue str =+ ANSI.setSGRCode [ANSI.SetConsoleIntensity ANSI.BoldIntensity] <>+ ANSI.setSGRCode [ANSI.SetColor ANSI.Foreground ANSI.Vivid ANSI.Blue] <>+ str <> ANSI.setSGRCode [ANSI.Reset]++tblue :: T.Text -> T.Text+tblue = dimap T.unpack T.pack blue+ red :: String -> String red str = ANSI.setSGRCode [ANSI.SetColor ANSI.Foreground ANSI.Vivid ANSI.Red] <>
+ src/Niv/Sources.hs view
@@ -0,0 +1,216 @@+{-# LANGUAGE DerivingStrategies #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE LambdaCase #-}++module Niv.Sources where++import Data.Aeson (FromJSON, FromJSONKey, ToJSON, ToJSONKey)+import Data.FileEmbed (embedFile)+import Data.Bifunctor (first)+import Data.Hashable (Hashable)+import Data.List+import Data.String.QQ (s)+import Niv.GitHub+import Niv.Logger+import Niv.Update+import System.FilePath ((</>))+import UnliftIO+import qualified Data.Aeson as Aeson+import qualified Data.Aeson.Extended as Aeson+import qualified Data.ByteString as B+import qualified Data.ByteString.Lazy.Char8 as BL8+import qualified Data.Digest.Pure.MD5 as MD5+import qualified Data.HashMap.Strict as HMS+import qualified Data.Text as T+import qualified System.Directory as Dir++-------------------------------------------------------------------------------+-- sources.json related+-------------------------------------------------------------------------------++data SourcesError+ = SourcesDoesntExist+ | SourceIsntJSON+ | SpecIsntAMap++newtype Sources = Sources+ { unSources :: HMS.HashMap PackageName PackageSpec }+ deriving newtype (FromJSON, ToJSON)++getSourcesEither :: IO (Either SourcesError Sources)+getSourcesEither = do+ Dir.doesFileExist pathNixSourcesJson >>= \case+ False -> pure $ Left SourcesDoesntExist+ True ->+ Aeson.decodeFileStrict pathNixSourcesJson >>= \case+ Just value -> case valueToSources value of+ Nothing -> pure $ Left SpecIsntAMap+ Just srcs -> pure $ Right srcs+ Nothing -> pure $ Left SourceIsntJSON+ where+ valueToSources :: Aeson.Value -> Maybe Sources+ valueToSources = \case+ Aeson.Object obj -> fmap (Sources . mapKeys PackageName) $ traverse+ (\case+ Aeson.Object obj' -> Just (PackageSpec obj')+ _ -> Nothing+ ) obj+ _ -> Nothing+ mapKeys :: (Eq k2, Hashable k2) => (k1 -> k2) -> HMS.HashMap k1 v -> HMS.HashMap k2 v+ mapKeys f = HMS.fromList . map (first f) . HMS.toList++getSources :: IO Sources+getSources = do+ warnIfOutdated+ getSourcesEither >>= either+ (\case+ SourcesDoesntExist -> abortSourcesDoesntExist+ SourceIsntJSON -> abortSourcesIsntJSON+ SpecIsntAMap -> abortSpecIsntAMap+ ) pure++setSources :: Sources -> IO ()+setSources sources = Aeson.encodeFilePretty pathNixSourcesJson sources++newtype PackageName = PackageName { unPackageName :: T.Text }+ deriving newtype (Eq, Hashable, FromJSONKey, ToJSONKey, Show)++newtype PackageSpec = PackageSpec { unPackageSpec :: Aeson.Object }+ deriving newtype (FromJSON, ToJSON, Show, Semigroup, Monoid)++-- | Simply discards the 'Freedom'+attrsToSpec :: Attrs -> PackageSpec+attrsToSpec = PackageSpec . fmap snd++-- | @nix/sources.json@+pathNixSourcesJson :: FilePath+pathNixSourcesJson = "nix" </> "sources.json"++--+-- ABORT messages+--++abortSourcesDoesntExist :: IO a+abortSourcesDoesntExist = abort $ T.unlines [ line1, line2 ]+ where+ line1 = "Cannot use " <> T.pack pathNixSourcesJson+ line2 = [s|+The sources file does not exist! You may need to run 'niv init'.+|]++abortSourcesIsntJSON :: IO a+abortSourcesIsntJSON = abort $ T.unlines [ line1, line2 ]+ where+ line1 = "Cannot use " <> T.pack pathNixSourcesJson+ line2 = "The sources file should be JSON."++abortSpecIsntAMap :: IO a+abortSpecIsntAMap = abort $ T.unlines [ line1, line2 ]+ where+ line1 = "Cannot use " <> T.pack pathNixSourcesJson+ line2 = [s|+The package specifications in the sources file should be JSON maps from+attribute name to attribute value, e.g.:+ { "nixpkgs": { "foo": "bar" } }+|]++-------------------------------------------------------------------------------+-- sources.nix related+-------------------------------------------------------------------------------+++-- | All the released versions of nix/sources.nix+data SourcesNixVersion+ = V1+ | V2+ | V3+ | V4+ | V5+ | V6+ | V7+ | V8+ | V9+ | V10+ deriving stock (Bounded, Enum, Eq)++-- | A user friendly version+sourcesVersionToText :: SourcesNixVersion -> T.Text+sourcesVersionToText = \case+ V1 -> "1"+ V2 -> "2"+ V3 -> "3"+ V4 -> "4"+ V5 -> "5"+ V6 -> "6"+ V7 -> "7"+ V8 -> "8"+ V9 -> "9"+ V10 -> "10"++latestVersionMD5 :: T.Text+latestVersionMD5 = sourcesVersionToMD5 maxBound++-- | Find a version based on the md5 of the nix/sources.nix+md5ToSourcesVersion :: T.Text -> Maybe SourcesNixVersion+md5ToSourcesVersion md5 =+ find (\snv -> sourcesVersionToMD5 snv == md5) [minBound .. maxBound]++-- | The MD5 sum of a particular version+sourcesVersionToMD5 :: SourcesNixVersion -> T.Text+sourcesVersionToMD5 = \case+ V1 -> "a7d3532c70fea66ffa25d6bc7ee49ad5"+ V2 -> "24cc0719fa744420a04361e23a3598d0"+ V3 -> "e01ed051e2c416e0fc7355fc72aeee3d"+ V4 -> "f754fe0e661b61abdcd32cb4062f5014"+ V5 -> "c34523590ff7dec7bf0689f145df29d1"+ V6 -> "8143f1db1e209562faf80a998be4929a"+ V7 -> "00a02cae76d30bbef96f001cabeed96f"+ V8 -> "e8b860753dd7fa1fd7b805dd836eb607"+ V9 -> "87149616c1b3b1e5aa73178f91c20b53"+ V10 -> "d8625c0a03dd935e1c79f46407faa8d3"++-- | The MD5 sum of ./nix/sources.nix+sourcesNixMD5 :: IO T.Text+sourcesNixMD5 = T.pack . show . MD5.md5 <$> BL8.readFile pathNixSourcesNix++-- | @nix/sources.nix@+pathNixSourcesNix :: FilePath+pathNixSourcesNix = "nix" </> "sources.nix"++warnIfOutdated :: IO ()+warnIfOutdated = do+ tryAny (BL8.readFile pathNixSourcesNix) >>= \case+ Left e -> tsay $ T.unlines -- warn with tsay+ [ T.unwords [ tyellow "WARNING:", "Could not read" , T.pack pathNixSourcesNix ]+ , T.unwords [ " ", "(", tshow e, ")" ]+ ]+ Right content -> do+ case md5ToSourcesVersion (T.pack $ show $ MD5.md5 content) of+ -- This is a custom or newer version, we don't do anything+ Nothing -> pure ()+ Just v+ -- The file is the latest+ | v == maxBound -> pure ()+ -- The file is older than than latest+ | otherwise -> do+ tsay $ T.unlines+ [ T.unwords+ [ tbold $ tblue "INFO:"+ , "new sources.nix available:"+ , sourcesVersionToText v, "->", sourcesVersionToText maxBound+ ]+ , " Please run 'niv init' or add the following line in the " <>+ T.pack pathNixSourcesNix <> " file:"+ , " # niv: no_update"+ ]++-- | Glue code between nix and sources.json+initNixSourcesNixContent :: B.ByteString+initNixSourcesNixContent = $(embedFile "nix/sources.nix")++-- | Empty JSON map+initNixSourcesJsonContent :: B.ByteString+initNixSourcesJsonContent = "{}"
+ src/Niv/Sources/Test.hs view
@@ -0,0 +1,13 @@+module Niv.Sources.Test where++import Niv.Sources+import Test.Tasty.HUnit ((@=?))+import qualified Data.ByteString.Lazy as BL+import qualified Data.Digest.Pure.MD5 as MD5+import qualified Data.Text as T++-- | Ensure that the sources.nix we ship is tracked as the latest version+test_shippedSourcesNixIsLatest :: IO ()+test_shippedSourcesNixIsLatest =+ latestVersionMD5 @=?+ (T.pack . show . MD5.md5 . BL.fromStrict $ initNixSourcesNixContent)
src/Niv/Test.hs view
@@ -1,5 +1,6 @@ module Niv.Test (tests, test) where +import Niv.Sources.Test import Niv.GitHub.Test import Niv.Update.Test import qualified Test.Tasty as Tasty@@ -26,5 +27,8 @@ , Tasty.testCase "updates once" test_githubUpdatesOnce , Tasty.testCase "doesn't override rev" test_githubDoesntOverrideRev , Tasty.testCase "falls back to URL" test_githubURLFallback+ ]+ , Tasty.testGroup "sources.nix"+ [ Tasty.testCase "has latest version" test_shippedSourcesNixIsLatest ] ]