diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,33 @@
 # Revision history for nvfetcher
 
+## 0.5.0.0
+
+There have been many significant changes since the last release.
+**Starting from this version, nvfetcher no longer requires shake database for each project,
+in other words, there is no need to commit the database in git or share it between machines.**
+Also, a machine-readable `generated.json` will be generated for convenience.
+
+### Migration
+
+The option in TOML configuration `cargo_lock` (string) was changed to `cargo_locks` (list of strings), since now nvfetcher supports handling multi-cargo locks.
+
+* Use `fetchFromGitHub` as the GitHub fetcher (introduces [`nix-prefetch`](https://github.com/msteen/nix-prefetch))
+* Add [cmd](https://nvchecker.readthedocs.io/en/latest/usage.html#find-with-a-command) version source
+* Support pinning a package
+* Tweak src name to extract .vsx file properly
+* Add option --filter to specify packages to update
+* Fix missing `argActionAfterBuild`
+* Add one shot nvchecker rule independent of package definition
+* Don't cache generated nix exprs in shake database
+* Add `fetchTarball`
+* Produce parser readable `generated.json`
+* Internalize shake database
+* Validate config before decoding
+* Extract `Config` from `Arg` and save it to shake extra
+* Breakdown `Args` to provide a more concise API
+* Support multi-cargo lock files
+* Pretty print rules in command line
+
 ## 0.4.0.0
 
 * Rename `_build` to `_sources`
diff --git a/Main_example.hs b/Main_example.hs
--- a/Main_example.hs
+++ b/Main_example.hs
@@ -15,29 +15,33 @@
 
   define $ package "feeluown-core" `fromPypi` "feeluown"
 
-  define $ package "qliveplayer" `fromGitHub'` ("IsoaSFlus", "QLivePlayer", fetchSubmodules .~ True)
-
   define $
     package "apple-emoji"
       `sourceManual` "0.0.0.20200413"
       `fetchUrl` const
-        "https://github.com/samuelngs/apple-emoji-linux/releases/download/latest/AppleColorEmoji.ttf"
+        "https://github.com/samuelngs/apple-emoji-linux/releases/download/alpha-release-v1.0.0/AppleColorEmoji.ttf"
 
   define $
     package "nvfetcher-git"
       `sourceGit` "https://github.com/berberman/nvfetcher"
       `fetchGitHub` ("berberman", "nvfetcher")
 
-  define $
-    package "vim"
-      `sourceWebpage` ("http://ftp.vim.org/pub/vim/patches/7.3/", "7\\.3\\.\\d+", id)
-      `fetchGitHub` ("vim", "vim")
-      `tweakVersion` (\v -> v & fromPattern ?~ "(.+)" & toPattern ?~ "v\\1")
+  -- define $
+  --   package "vim"
+  --     `sourceWebpage` ("http://ftp.vim.org/pub/vim/patches/7.3/", "7\\.3\\.\\d+", id)
+  --     `fetchGitHub` ("vim", "vim")
+  --     `tweakVersion` (\v -> v & fromPattern ?~ "(.+)" & toPattern ?~ "v\\1")
 
   define $
     package "rust-git-dependency-example"
       `sourceManual` "8a5f37a8f80a3b05290707febf57e88661cee442"
       `fetchGit` "https://gist.github.com/NickCao/6c4dbc4e15db5da107de6cdb89578375"
-      `hasCargoLock` "Cargo.lock"
+      `hasCargoLocks` ["Cargo.lock"]
 
   define $ package "vscode-LiveServer" `fromOpenVsx` ("ritwickdey", "LiveServer")
+
+  define $
+    package "revda"
+      `sourceGit` "https://github.com/THMonster/Revda"
+      `fetchGitHub'` ("THMonster", "Revda", fetchSubmodules .~ True)
+      `hasCargoLocks` ["dmlive/Cargo.lock", "dmlive/tars-stream/Cargo.lock"]
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -14,58 +14,61 @@
 
 ```toml
 # nvfetcher.toml
-[feeluown-core]
+[feeluown]
 src.pypi = "feeluown"
 fetch.pypi = "feeluown"
-
-[qliveplayer]
-src.github = "IsoaSFlus/QLivePlayer"
-fetch.github = "IsoaSFlus/QLivePlayer"
-git.fetchSubmodules = true
 ```
 
-it can create `sources.nix` like:
+it will create `_sources/generated.nix`:
 
 ```nix
-# sources.nix
-{ fetchgit, fetchurl }:
+{ fetchgit, fetchurl, fetchFromGitHub }:
 {
-  feeluown-core = {
-    pname = "feeluown-core";
-    version = "3.7.7";
+  feeluown = {
+    pname = "feeluown";
+    version = "3.8.2";
     src = fetchurl {
-      sha256 = "06d3j39ff9znqxkhp9ly81lcgajkhg30hyqxy2809yn23xixg3x2";
-      url = "https://pypi.io/packages/source/f/feeluown/feeluown-3.7.7.tar.gz";
-    };
-  };
-  qliveplayer = {
-    pname = "qliveplayer";
-    version = "3.22.1";
-    src = fetchgit {
-      url = "https://github.com/IsoaSFlus/QLivePlayer";
-      rev = "3.22.1";
-      fetchSubmodules = true;
-      deepClone = false;
-      leaveDotGit = false;
-      sha256 = "00zqg28q5xrbgql0kclgkhd15fc02qzsrvi0qg8lg3qf8a53v263";
+      url = "https://pypi.io/packages/source/f/feeluown/feeluown-3.8.2.tar.gz";
+      sha256 = "sha256-V2yzpkmjRkipZOvQGB2mYRhiiEly6QPrTOMJ7BmyWBQ=";
     };
   };
 }
 ```
 
+and `_sources/generated.json`:
+
+```json
+{
+  "feeluown": {
+    "pinned": false,
+    "cargoLocks": null,
+    "name": "feeluown-core",
+    "version": "3.8.2",
+    "passthru": null,
+    "src": {
+      "url": "https://pypi.io/packages/source/f/feeluown/feeluown-3.8.2.tar.gz",
+      "name": null,
+      "type": "url",
+      "sha256": "sha256-V2yzpkmjRkipZOvQGB2mYRhiiEly6QPrTOMJ7BmyWBQ="
+    },
+    "extract": null,
+    "rustGitDeps": null
+  }
+}
+```
+
 We tell nvfetcher how to get the latest version number of packages and how to fetch their sources given version numbers,
-and nvfetcher will help us keep their version and prefetched SHA256 sums up-to-date, stored in `_sources/generated.nix`.
-Shake will handle necessary rebuilds as long as you keep `_sources` directory -- we check versions of packages during each run, but only prefetch them when needed.
+and nvfetcher will help us keep their version and prefetched SHA256 sums up-to-date, producing ready-to-use nix expressions in `_sources/generated.nix`.
+Nvfetcher reads `generated.json` to produce version change message, such as `feeluown: 3.8.1 → 3.8.2`.
+We always check versions of packages during each run, but only do prefetch and further operations when needed.
 
 ### Live examples
 
 How to use the generated sources file? Here are several examples:
 
-* [DevOS](https://github.com/divnix/devos/tree/main/pkgs) - Packages are defined in TOML
-
-* My [flakes repo](https://github.com/berberman/flakes) - Packages are defined in eDSL
+- My [flakes repo](https://github.com/berberman/flakes)
 
-* Nick Cao's [flakes repo](https://gitlab.com/NickCao/flakes/-/tree/master/pkgs) - Packages are defined in TOML
+- Nick Cao's [flakes repo](https://gitlab.com/NickCao/flakes/-/tree/master/pkgs)
 
 ## Installation
 
@@ -103,9 +106,10 @@
 Available options:
   --version                Show version
   --help                   Show this help text
-  -o,--build-dir FILE      Directory that nvfetcher puts artifacts to
+  -o,--build-dir DIR       Directory that nvfetcher puts artifacts to
                            (default: "_sources")
-  --commit-changes         `git commit` changes in this run (with shake db)
+  --commit-changes         `git commit` build dir with version changes as commit
+                           message
   -l,--changelog FILE      Dump version changes to a file
   -j NUM                   Number of threads (0: detected number of processors)
                            (default: 0)
@@ -113,88 +117,100 @@
                            nix-instantiate, etc.) (default: 3)
   -t,--timing              Show build time
   -v,--verbose             Verbose mode
+  -f,--filter REGEX        Regex to filter packages to be updated
   TARGET                   Two targets are available: 1.build 2.clean
                            (default: "build")
   -c,--config FILE         Path to nvfetcher TOML config
                            (default: "nvfetcher.toml")
 ```
 
-Each *package* corresponds to a TOML table, whose name is encoded as table key, with
+Each _package_ corresponds to a TOML table, whose name is encoded as table key, with
 two required fields and three optional fields in each table.
 You can find an example of the configuration file, see [`nvfetcher_example.toml`](nvfetcher_example.toml).
 
 #### Nvchecker
 
 Version source -- how do we track upstream version updates?
-* `src.github = owner/repo` - the latest github release
-* `src.github_tag = owner/repo` - the max github tag, usually used with list options (see below)
-* `src.pypi = pypi_name` - the latest pypi release
-* `src.git = git_url` (and an optional `src.branch = git_branch`) - **the latest commit** of a repo
-* `src.archpkg = archlinux_pkg_name` -- the latest version of an archlinux package
-* `src.aur = aur_pkg_name` -- the latest version of an aur package
-* `src.manual = v` -- a fixed version, which never updates
-* `src.repology = project:repo` -- the latest version from repology
-* `src.webpage = web_url` and `src.regex` -- a string in webpage that matches with regex
-* `src.httpheader = request_url` and `src.regex` -- a string in http header that matches with regex
-* `src.openvsx = publisher.ext_name` -- the latest version of a vscode extension from open vsx
-* `src.vsmarketplace = publisher.ext_name` -- the latest version of a vscode extension from vscode marketplace
 
+- `src.github = owner/repo` - the latest github release
+- `src.github_tag = owner/repo` - the max github tag, usually used with list options (see below)
+- `src.pypi = pypi_name` - the latest pypi release
+- `src.git = git_url` (and an optional `src.branch = git_branch`) - **the latest commit** of a repo
+- `src.archpkg = archlinux_pkg_name` -- the latest version of an archlinux package
+- `src.aur = aur_pkg_name` -- the latest version of an aur package
+- `src.manual = v` -- a fixed version, which never updates
+- `src.repology = project:repo` -- the latest version from repology
+- `src.webpage = web_url` and `src.regex` -- a string in webpage that matches with regex
+- `src.httpheader = request_url` and `src.regex` -- a string in http header that matches with regex
+- `src.openvsx = publisher.ext_name` -- the latest version of a vscode extension from open vsx
+- `src.vsmarketplace = publisher.ext_name` -- the latest version of a vscode extension from vscode marketplace
+- `src.cmd = cmd` -- the version from a shell command (e.g. `echo Meow`)
 
 Optional list options for some version sources (`src.github_tag`, `src.webpage`, and `src.httpheader`),
 see the corresponding [nvchecker documentation](https://nvchecker.readthedocs.io/en/latest/usage.html#list-options) for details.
-* `src.include_regex`
-* `src.exclude_regex`
-* `src.sort_version_key`
-* `src.ignored`
 
+- `src.include_regex`
+- `src.exclude_regex`
+- `src.sort_version_key`
+- `src.ignored`
 
 Optional global options for all kinds of version sources,
 see the corresponding [nvchecker documentation](https://nvchecker.readthedocs.io/en/latest/usage.html#global-options) for details. You can tweak obtained version number using this option, e.g. stripping the prefix `v` or transforming the result by regex.
-* `src.prefix`
-* `src.from_pattern`
-* `src.to_pattern`
 
+- `src.prefix`
+- `src.from_pattern`
+- `src.to_pattern`
+
 #### Nix fetcher
 
 How do we fetch the package source if we have the target version number?
 `$ver` is available in string, which will be set to the result of nvchecker.
 
-* `fetch.github = owner/repo`
-* `fetch.pypi = pypi_name`
-* `fetch.git = git_url`
-* `fetch.url = url`
-* `fetch.openvsx = publisher.ext_name`
-* `fetch.vsmarketplace = publisher.ext_name`
-
+- `fetch.github = owner/repo`
+- `fetch.pypi = pypi_name`
+- `fetch.git = git_url`
+- `fetch.url = url`
+- `fetch.openvsx = publisher.ext_name`
+- `fetch.vsmarketplace = publisher.ext_name`
+- `fetch.tarball = tarball_url`
 
-Optional `nix-prefetch-git` config, which make sense only when the fetcher equals to `fetch.github` or `fetch.git`.
-They can exist simultanesouly.
-  * `git.deepClone`
-  * `git.fetchSubmodules`
-  * `git.leaveDotGit`
+Optional `nix-prefetch fetchgit` config, which make sense only when the fetcher equals to `fetch.github` or `fetch.git`.
+They can exist simultaneously.
 
+- `git.deepClone`
+- `git.fetchSubmodules`
+- `git.leaveDotGit`
 
 #### Extract src
 
-Optional *extract src* config, files are extracted into build directory, and then read by `readFile` in generated nix expr.
-  * `extract = [ "file_1", "file_2", ...]` - file paths are relative to the source root
+Optional _extract src_ config, files are extracted into build directory, and then read by `readFile` in generated nix expr.
 
+- `extract = [ "file_1", "file_2", ...]` - file paths are relative to the source root
+
 #### Rust support
 
 `rustPlatform.buildRustPackage` now accepts an attribute [`cargoLock`](https://github.com/NixOS/nixpkgs/blob/master/doc/languages-frameworks/rust.section.md#importing-a-cargolock-file) to vendor dependencies from `Cargo.lock`,
-so we can use this instead TOFU `cargoSha256` for Rust packageing. `nvfetcher` supports automating this process,
-extracting the lock file to build and calculating `cargoLock.outputHashes`, as long as you set the config
-* `cargo_lock = cargo_lock_path` - relative to the source root
+so we can use this instead TOFU `cargoSha256` for Rust packaging. `nvfetcher` supports automating this process,
+extracting the lock file to build and calculating `cargoLock.outputHashes`, as long as you set the config.
+There can be many lock files in one source.
 
+- `cargo_locks = [ "cargo_lock_path_1", "cargo_lock_path_2", ...]` - relative to the source root
+
 #### Passthru
 
-*passthru* config, an additional set of attrs to be generated.
+_passthru_ config, an additional set of attrs to be generated.
 
-  * `passthru = { k1 = "v1", k2 = "v2", ... }`
+- `passthru = { k1 = "v1", k2 = "v2", ... }`
 
+#### Pinned
+
+If a package is pinned, we call nvchecker to check the new version iff there's no existing version.
+
+- `pinned = true`
+
 ### Haskell library
 
-nvfetcher itsetlf is a Haskell library as well, whereas the CLI program is just a trivial wrapper of the library.
+nvfetcher itself is a Haskell library as well, whereas the CLI program is just a trivial wrapper of the library.
 You can create a Haskell program depending on it directly, by using the `runNvFetcher` entry point.
 In this case, we can define packages in Haskell language, getting rid of TOML constraints.
 
diff --git a/app/Config.hs b/app/Config.hs
--- a/app/Config.hs
+++ b/app/Config.hs
@@ -1,6 +1,4 @@
 {-# LANGUAGE DataKinds #-}
-{-# LANGUAGE DeriveAnyClass #-}
-{-# LANGUAGE DeriveGeneric #-}
 {-# LANGUAGE LambdaCase #-}
 {-# LANGUAGE OverloadedStrings #-}
 {-# LANGUAGE RecordWildCards #-}
@@ -11,46 +9,70 @@
 import Config.PackageFetcher
 import Config.VersionSource
 import Data.Coerce (coerce)
+import Data.Either.Extra (mapLeft)
+import qualified Data.HashMap.Strict as HMap
+import Data.List (intersect)
 import Data.List.NonEmpty (NonEmpty ((:|)))
 import qualified Data.List.NonEmpty as NE
+import Data.Text (Text)
+import qualified Data.Text as T
 import NvFetcher.Types
 import Toml
 import Validation (validationToEither)
 
-parseConfig :: TOML -> Either [Toml.TomlDecodeError] [Package]
-parseConfig toml = go tables [] []
+data PackageConfigParseError = TomlErrors [TomlDecodeError] | KeyConflicts [[Key]]
+
+instance Semigroup PackageConfigParseError where
+  TomlErrors e <> _ = TomlErrors e
+  _ <> TomlErrors e = TomlErrors e
+  KeyConflicts xs <> KeyConflicts ys = KeyConflicts $ xs <> ys
+
+prettyPackageConfigParseError :: PackageConfigParseError -> Text
+prettyPackageConfigParseError (TomlErrors e) = prettyTomlDecodeErrors e
+prettyPackageConfigParseError (KeyConflicts xs) = "Skip parsing!\n" <> T.unlines ["Key conflict: " <> T.intercalate ", " [prettyKey k | k <- ks] | ks <- xs]
+
+parseConfig :: TOML -> Either PackageConfigParseError [Package]
+parseConfig toml = go tables Nothing []
   where
-    go (Left errs : xs) se sp = go xs (se <> errs) sp
+    go (Left errs : xs) (Just se) sp = go xs (Just (se <> errs)) sp
+    go (Left errs : xs) Nothing sp = go xs (Just errs) sp
     go (Right x : xs) se sp = go xs se (x : sp)
-    go [] [] sp = Right sp
-    go [] se _ = Left se
+    go [] Nothing sp = Right sp
+    go [] (Just se) _ = Left se
     tables =
-      [ fmap (toPackage (coerce k)) $
-          validationToEither $
-            Toml.runTomlCodec packageConfigCodec v
+      [ fmap (toPackage (coerce k)) $ validateKeys v >> mapLeft TomlErrors (validationToEither (Toml.runTomlCodec packageConfigCodec v))
         | (Toml.unKey -> (Toml.unPiece -> k) :| _, v) <- Toml.toList $ Toml.tomlTables toml
       ]
 
+validateKeys :: TOML -> Either PackageConfigParseError ()
+validateKeys toml = if null e then Right () else Left $ foldl1 (<>) e
+  where
+    allKeys = HMap.keys $ Toml.tomlPairs toml
+    go xs = let intersection = xs `intersect` allKeys in if length intersection > 1 then intersection else []
+    e = [KeyConflicts [t] | k <- [versionSourceKeys, fetcherKeys], let t = go k, not $ null t]
+
 --------------------------------------------------------------------------------
 
 data PackageConfig = PackageConfig
   { pcVersionSource :: VersionSource,
     pcFetcher :: PackageFetcher,
     pcExtractFiles :: Maybe PackageExtractSrc,
-    pcCargoLockPath :: Maybe PackageCargoFilePath,
+    pcCargoLockFiles :: Maybe PackageCargoLockFiles,
     pcNvcheckerOptions :: NvcheckerOptions,
-    pcPassthru :: PackagePassthru
+    pcPassthru :: PackagePassthru,
+    pcUseStale :: UseStaleVersion
   }
 
 toPackage :: PackageKey -> PackageConfig -> Package
 toPackage k PackageConfig {..} =
   Package
     (coerce k)
-    (NvcheckerQ pcVersionSource pcNvcheckerOptions)
+    (CheckVersion pcVersionSource pcNvcheckerOptions)
     pcFetcher
     pcExtractFiles
-    pcCargoLockPath
+    pcCargoLockFiles
     pcPassthru
+    pcUseStale
 
 packageConfigCodec :: TomlCodec PackageConfig
 packageConfigCodec =
@@ -58,9 +80,10 @@
     <$> versionSourceCodec .= pcVersionSource
     <*> fetcherCodec .= pcFetcher
     <*> extractFilesCodec .= pcExtractFiles
-    <*> cargoLockPathCodec .= pcCargoLockPath
+    <*> cargoLockPathCodec .= pcCargoLockFiles
     <*> nvcheckerOptionsCodec .= pcNvcheckerOptions
     <*> passthruCodec .= pcPassthru
+    <*> pinnedCodec .= pcUseStale
 
 --------------------------------------------------------------------------------
 
@@ -71,8 +94,12 @@
     (\mxs -> coerce <$> (mxs >>= NE.nonEmpty))
     $ dioptional $ arrayOf _String "extract"
 
-cargoLockPathCodec :: TomlCodec (Maybe PackageCargoFilePath)
-cargoLockPathCodec = dioptional $ diwrap (string "cargo_lock")
+cargoLockPathCodec :: TomlCodec (Maybe PackageCargoLockFiles)
+cargoLockPathCodec =
+  dimap
+    (fmap (NE.toList . coerce))
+    (\mxs -> coerce <$> (mxs >>= NE.nonEmpty))
+    $ dioptional $ arrayOf _String "cargo_locks"
 
 nvcheckerOptionsCodec :: TomlCodec NvcheckerOptions
 nvcheckerOptionsCodec =
@@ -83,3 +110,14 @@
 
 passthruCodec :: TomlCodec PackagePassthru
 passthruCodec = diwrap $ tableHashMap _KeyText text "passthru"
+
+pinnedCodec :: TomlCodec UseStaleVersion
+pinnedCodec =
+  dimap
+    ( \case
+        PermanentStale -> Just True
+        TemporaryStale -> error "Impossible!"
+        NoStale -> Just False
+    )
+    (maybe NoStale (\x -> if x then PermanentStale else NoStale))
+    $ dioptional $ bool "pinned"
diff --git a/app/Config/PackageFetcher.hs b/app/Config/PackageFetcher.hs
--- a/app/Config/PackageFetcher.hs
+++ b/app/Config/PackageFetcher.hs
@@ -5,7 +5,11 @@
 {-# LANGUAGE RecordWildCards #-}
 {-# LANGUAGE ViewPatterns #-}
 
-module Config.PackageFetcher (fetcherCodec) where
+module Config.PackageFetcher
+  ( fetcherCodec,
+    fetcherKeys,
+  )
+where
 
 import Data.Coerce (coerce)
 import Data.Default (Default, def)
@@ -31,9 +35,21 @@
       openVsxCodec,
       vscodeMarketplaceCodec,
       gitCodec,
-      urlCodec
+      urlCodec,
+      tarballCodec
     ]
 
+fetcherKeys :: [Key]
+fetcherKeys =
+  [ "fetch.github",
+    "fetch.pypi",
+    "fetch.openvsx",
+    "fetch.vsmarketplace",
+    "fetch.git",
+    "fetch.url",
+    "fetch.tarball"
+  ]
+
 --------------------------------------------------------------------------------
 
 data GitOptions = GitOptions
@@ -59,7 +75,15 @@
         & leaveDotGit .~ fromMaybe False goLeaveDotGit
   )
     <$> f (GitOptions (Just _deepClone) (Just _fetchSubmodules) (Just _leaveDotGit))
-_GitOptions _ x@FetchUrl {} = pure x
+_GitOptions f x@FetchGitHub {..} =
+  ( \GitOptions {..} ->
+      x
+        & deepClone .~ fromMaybe False goDeepClone
+        & fetchSubmodules .~ fromMaybe False goFetchSubmodules
+        & leaveDotGit .~ fromMaybe False goLeaveDotGit
+  )
+    <$> f (GitOptions (Just _deepClone) (Just _fetchSubmodules) (Just _leaveDotGit))
+_GitOptions _ x = pure x
 
 --------------------------------------------------------------------------------
 
@@ -138,3 +162,12 @@
     unsupportError
     (\t -> Right $ \(coerce -> v) -> urlFetcher $ T.replace "$ver" v t)
     "fetch.url"
+
+--------------------------------------------------------------------------------
+
+tarballCodec :: TomlCodec PackageFetcher
+tarballCodec =
+  Toml.textBy
+    unsupportError
+    (\t -> Right $ \(coerce -> v) -> tarballFetcher $ T.replace "$ver" v t)
+    "fetch.tarball"
diff --git a/app/Config/VersionSource.hs b/app/Config/VersionSource.hs
--- a/app/Config/VersionSource.hs
+++ b/app/Config/VersionSource.hs
@@ -2,7 +2,11 @@
 {-# LANGUAGE OverloadedStrings #-}
 {-# LANGUAGE RecordWildCards #-}
 
-module Config.VersionSource (versionSourceCodec) where
+module Config.VersionSource
+  ( versionSourceCodec,
+    versionSourceKeys,
+  )
+where
 
 import Data.Foldable (asum)
 import Data.Text (Text)
@@ -28,9 +32,26 @@
       webpageCodec,
       httpHeaderCodec,
       openVsxCodec,
-      vscodeMarketplaceCodec
+      vscodeMarketplaceCodec,
+      cmdCodec
     ]
 
+versionSourceKeys :: [Key]
+versionSourceKeys =
+  [ "src.github",
+    "src.github_tag",
+    "src.git",
+    "src.pypi",
+    "src.archpkg",
+    "src.aur",
+    "src.manual",
+    "src.webpage",
+    "src.httpheader",
+    "src.openvsx",
+    "src.vsmarketplace",
+    "src.cmd"
+  ]
+
 --------------------------------------------------------------------------------
 
 githubICodec :: Key -> TomlCodec (Text, Text)
@@ -215,3 +236,11 @@
 
 vscodeMarketplaceCodec :: TomlCodec VersionSource
 vscodeMarketplaceCodec = dimatch matchVscodeMarketplace (uncurry VscodeMarketplace) vscodeMarketplaceICodec
+
+--------------------------------------------------------------------------------
+
+matchCmd :: VersionSource -> Maybe Text
+matchCmd x = x ^? vcmd
+
+cmdCodec :: TomlCodec VersionSource
+cmdCodec = dimatch matchCmd Cmd (text "src.cmd")
diff --git a/app/Main.hs b/app/Main.hs
--- a/app/Main.hs
+++ b/app/Main.hs
@@ -1,11 +1,9 @@
 {-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE RecordWildCards #-}
-{-# LANGUAGE TemplateHaskell #-}
-{-# LANGUAGE ViewPatterns #-}
 
 module Main where
 
 import Config
+import Data.Default (def)
 import qualified Data.Text as T
 import qualified Data.Text.IO as T
 import NvFetcher
@@ -34,5 +32,5 @@
   case toml of
     Left e -> error $ T.unpack $ Toml.prettyTomlDecodeError $ Toml.ParseError e
     Right x -> case parseConfig x of
-      Left e -> error $ T.unpack $ Toml.prettyTomlDecodeErrors e
-      Right pkgs -> runNvFetcherNoCLI (cliOptionsToArgs opt) $ purePackageSet pkgs
+      Left e -> error $ T.unpack $ prettyPackageConfigParseError e
+      Right pkgs -> runNvFetcherNoCLI (applyCliOptions def opt) (optTarget opt) $ purePackageSet pkgs
diff --git a/nvfetcher.cabal b/nvfetcher.cabal
--- a/nvfetcher.cabal
+++ b/nvfetcher.cabal
@@ -1,6 +1,6 @@
 cabal-version:   2.4
 name:            nvfetcher
-version:         0.4.0.0
+version:         0.5.0.0
 synopsis:
   Generate nix sources expr for the latest version of packages
 
@@ -13,7 +13,7 @@
 license-file:    LICENSE
 author:          berberman
 maintainer:      berberman <berberman.yandex.com>
-copyright:       2021 berberman
+copyright:       2021-2022 berberman
 category:        Nix
 build-type:      Simple
 extra-doc-files:
@@ -26,8 +26,9 @@
 
 common common-options
   build-depends:
-    , aeson                 ^>=1.5.6
-    , base                  >=4.8    && <5
+    , aeson                 >=1.5.6   && <2.1
+    , aeson-pretty
+    , base                  >=4.8     && <5
     , binary
     , binary-instances      ^>=1.0.1
     , bytestring
@@ -40,6 +41,8 @@
     , neat-interpolation    ^>=0.5.1
     , optparse-simple       ^>=0.1.1
     , parsec
+    , prettyprinter
+    , regex-tdfa            ^>=1.3.1.1
     , shake                 ^>=0.19.4
     , text
     , tomland               ^>=1.3.2
@@ -60,6 +63,7 @@
   other-modules:   NvFetcher.Utils
   exposed-modules:
     NvFetcher
+    NvFetcher.Config
     NvFetcher.Core
     NvFetcher.ExtractSrc
     NvFetcher.FetchRustGitDeps
diff --git a/src/NvFetcher.hs b/src/NvFetcher.hs
--- a/src/NvFetcher.hs
+++ b/src/NvFetcher.hs
@@ -1,10 +1,12 @@
+{-# LANGUAGE LambdaCase #-}
 {-# LANGUAGE OverloadedStrings #-}
 {-# LANGUAGE QuasiQuotes #-}
 {-# LANGUAGE RecordWildCards #-}
+{-# LANGUAGE TupleSections #-}
 {-# LANGUAGE TypeApplications #-}
 {-# LANGUAGE ViewPatterns #-}
 
--- | Copyright: (c) 2021 berberman
+-- | Copyright: (c) 2021-2022 berberman
 -- SPDX-License-Identifier: MIT
 -- Maintainer: berberman <berberman@yandex.com>
 -- Stability: experimental
@@ -20,12 +22,12 @@
 -- import NvFetcher
 --
 -- main :: IO ()
--- main = runNvFetcher defaultArgs packageSet
+-- main = runNvFetcher packageSet
 --
 -- packageSet :: PackageSet ()
 -- packageSet = do
 --   define $ package "feeluown-core" `fromPypi` "feeluown"
---   define $ package "qliveplayer" `fromGitHub` ("IsoaSFlus", "QLivePlayer")
+--   define $ package "qliveplayer" `fromGitHub` ("THMonster", "QLivePlayer")
 -- @
 --
 -- You can find more examples of packages in @Main_example.hs@.
@@ -35,95 +37,76 @@
 -- * @main@ -- abbreviation of @main build@
 -- * @main build@ -- build nix sources expr from given @packageSet@
 -- * @main clean@ -- delete .shake dir and generated nix file
--- * @main -j@ -- build with parallelism
 --
 -- All shake options are inherited.
 module NvFetcher
-  ( Args (..),
-    defaultArgs,
-    runNvFetcher,
+  ( runNvFetcher,
+    runNvFetcher',
     runNvFetcherNoCLI,
-    cliOptionsToArgs,
+    applyCliOptions,
+    parseLastVersions,
     module NvFetcher.PackageSet,
     module NvFetcher.Types,
     module NvFetcher.Types.ShakeExtras,
   )
 where
 
-import Control.Monad.Extra (when, whenJust)
+import Control.Monad.Extra (forM_, when, whenJust)
+import qualified Data.Aeson as A
+import qualified Data.Aeson.Encode.Pretty as A
+import qualified Data.Aeson.Types as A
+import qualified Data.ByteString.Lazy.Char8 as LBS
+import Data.Default
+import Data.List ((\\))
+import qualified Data.Map.Strict as Map
+import Data.Maybe (catMaybes, fromMaybe)
 import Data.Text (Text)
 import qualified Data.Text as T
 import Development.Shake
 import Development.Shake.FilePath
 import NeatInterpolation (trimming)
+import NvFetcher.Config
 import NvFetcher.Core
+import NvFetcher.NixExpr (ToNixExpr (toNixExpr))
 import NvFetcher.NixFetcher
 import NvFetcher.Nvchecker
 import NvFetcher.Options
 import NvFetcher.PackageSet
 import NvFetcher.Types
 import NvFetcher.Types.ShakeExtras
-import NvFetcher.Utils (getShakeDir)
-
--- | Arguments for running nvfetcher
-data Args = Args
-  { -- | Shake options
-    argShakeOptions :: ShakeOptions,
-    -- | Build target
-    argTarget :: String,
-    -- | Shake dir
-    argBuildDir :: FilePath,
-    -- | Custom rules
-    argRules :: Rules (),
-    -- | Action run after build rule
-    argActionAfterBuild :: Action (),
-    -- | Action run after clean rule
-    argActionAfterClean :: Action (),
-    -- | Retry times
-    argRetries :: Int
-  }
-
--- | Default arguments of 'defaultMain'
---
--- Build dir is @_sources@.
-defaultArgs :: Args
-defaultArgs =
-  Args
-    ( shakeOptions
-        { shakeProgress = progressSimple,
-          shakeThreads = 0
-        }
-    )
-    "build"
-    "_sources"
-    (pure ())
-    (pure ())
-    (pure ())
-    3
+import NvFetcher.Utils (aesonKey, getDataDir)
+import qualified System.Directory.Extra as D
+import Text.Regex.TDFA ((=~))
 
 -- | Run nvfetcher with CLI options
 --
--- This function calls 'runNvFetcherNoCLI', using 'Args' from 'CLIOptions'.
+-- This function calls 'runNvFetcherNoCLI', using 'def' 'Config' overridden by 'CLIOptions'.
 -- Use this function to create your own Haskell executable program.
 runNvFetcher :: PackageSet () -> IO ()
-runNvFetcher packageSet =
-  getCLIOptions cliOptionsParser >>= flip runNvFetcherNoCLI packageSet . cliOptionsToArgs
+runNvFetcher = runNvFetcher' def
 
--- | Apply 'CLIOptions' to 'defaultArgs'
-cliOptionsToArgs :: CLIOptions -> Args
-cliOptionsToArgs CLIOptions {..} =
-  defaultArgs
-    { argActionAfterBuild = do
-        whenJust logPath logChangesToFile
-        when commit commitChanges,
-      argTarget = target,
-      argShakeOptions =
-        (argShakeOptions defaultArgs)
-          { shakeTimings = timing,
-            shakeVerbosity = if verbose then Verbose else Info,
-            shakeThreads = threads,
-            shakeFiles = buildDir
-          }
+-- | Similar to 'runNvFetcher', but uses custom @config@ instead of 'def' overridden by 'CLIOptions'
+runNvFetcher' :: Config -> PackageSet () -> IO ()
+runNvFetcher' config packageSet =
+  getCLIOptions cliOptionsParser >>= \cli -> runNvFetcherNoCLI (applyCliOptions config cli) (optTarget cli) packageSet
+
+-- | Apply 'CLIOptions' to 'Config'
+applyCliOptions :: Config -> CLIOptions -> Config
+applyCliOptions config CLIOptions {..} =
+  config
+    { buildDir = optBuildDir,
+      actionAfterBuild = do
+        whenJust optLogPath logChangesToFile
+        when optCommit commitChanges
+        actionAfterBuild config,
+      shakeConfig =
+        (shakeConfig config)
+          { shakeTimings = optTiming,
+            shakeVerbosity = if optVerbose then Verbose else Info,
+            shakeThreads = optThreads
+          },
+      filterRegex = optPkgNameFilter,
+      retry = optRetry
     }
 
 logChangesToFile :: FilePath -> Action ()
@@ -140,54 +123,98 @@
         [] -> Nothing
   whenJust commitMsg $ \msg -> do
     putInfo "Commiting changes"
-    getShakeDir >>= \dir -> command_ [] "git" ["add", dir]
+    getBuildDir >>= \dir -> command_ [] "git" ["add", dir]
     command_ [] "git" ["commit", "-m", msg]
 
+-- | @Parse generated.nix@
+parseLastVersions :: FilePath -> IO (Maybe (Map.Map PackageKey Version))
+parseLastVersions jsonFile =
+  D.doesFileExist jsonFile >>= \case
+    True -> do
+      objs <- A.decodeFileStrict' jsonFile
+      pure $
+        flip fmap objs $
+          ( \xs ->
+              Map.fromList
+                . catMaybes
+                $ [(PackageKey k,) <$> A.parseMaybe (A..: "version") obj | (k, obj) <- xs]
+          )
+            . Map.toList
+    _ -> pure mempty
+
 -- | Entry point of nvfetcher
-runNvFetcherNoCLI :: Args -> PackageSet () -> IO ()
-runNvFetcherNoCLI args@Args {..} packageSet = do
-  pkgs <- runPackageSet packageSet
-  shakeExtras <- initShakeExtras pkgs argRetries
-  let opts =
-        argShakeOptions
-          { shakeExtra = addShakeExtra shakeExtras (shakeExtra argShakeOptions)
+runNvFetcherNoCLI :: Config -> Target -> PackageSet () -> IO ()
+runNvFetcherNoCLI config@Config {..} target packageSet = do
+  pkgs <- Map.map pinIfUnmatch <$> runPackageSet packageSet
+  lastVersions <- parseLastVersions $ buildDir </> generatedJsonFileName
+  shakeDir <- getDataDir
+  -- Set shakeFiles
+  let shakeOptions1 = shakeConfig {shakeFiles = shakeDir}
+  -- shakeConfig in Config will be shakeOptions1 (not including shake extra)
+  shakeExtras <- initShakeExtras (config {shakeConfig = shakeOptions1}) pkgs $ fromMaybe mempty lastVersions
+  -- Set shakeExtra
+  let shakeOptions2 = shakeOptions1 {shakeExtra = addShakeExtra shakeExtras (shakeExtra shakeConfig)}
+      rules = mainRules config
+  shake shakeOptions2 $ want [show target] >> rules
+  where
+    -- Don't touch already pinned packages
+    pinIfUnmatch x@Package {..}
+      | Just regex <- filterRegex =
+        x
+          { _ppinned = case _ppinned of
+              PermanentStale -> PermanentStale
+              _ ->
+                if _pname =~ regex
+                  then NoStale
+                  else TemporaryStale
           }
-      rules = mainRules args
-  shake opts $ want [argTarget] >> rules
+      | otherwise = x
 
 --------------------------------------------------------------------------------
 
-mainRules :: Args -> Rules ()
-mainRules Args {..} = do
+mainRules :: Config -> Rules ()
+mainRules Config {..} = do
   "clean" ~> do
-    getShakeDir >>= flip removeFilesAfter ["//*"]
-    argActionAfterClean
+    getBuildDir >>= flip removeFilesAfter ["//*"]
+    actionAfterClean
 
   "build" ~> do
     allKeys <- getAllPackageKeys
-    body <- parallel $ generateNixSourceExpr <$> allKeys
+    results <- parallel $ runPackage <$> allKeys
+    -- Record removed packages to version changes
+    getAllOnDiskVersions
+      >>= \oldPkgs -> forM_ (Map.keys oldPkgs \\ allKeys) $
+        \pkg -> recordVersionChange (coerce pkg) (oldPkgs Map.!? pkg) "∅"
     getVersionChanges >>= \changes ->
       if null changes
         then putInfo "Up to date"
         else do
           putInfo "Changes:"
           putInfo $ unlines $ show <$> changes
-    shakeDir <- getShakeDir
-    let genPath = shakeDir </> "generated.nix"
-    putVerbose $ "Generating " <> genPath
-    writeFileChanged genPath $ T.unpack $ srouces (T.unlines body) <> "\n"
-    need [genPath]
-    argActionAfterBuild
+    buildDir <- getBuildDir
+    let generatedNixPath = buildDir </> generatedNixFileName
+        generatedJSONPath = buildDir </> generatedJsonFileName
+    putVerbose $ "Generating " <> generatedNixPath
+    writeFileChanged generatedNixPath $ T.unpack $ srouces (T.unlines $ toNixExpr <$> results) <> "\n"
+    putVerbose $ "Generating " <> generatedJSONPath
+    writeFileChanged generatedJSONPath $ LBS.unpack $ A.encodePretty $ A.object [aesonKey (_prname r) A..= r | r <- results]
+    actionAfterBuild
 
-  argRules
+  customRules
   coreRules
 
 srouces :: Text -> Text
 srouces body =
   [trimming|
     # This file was generated by nvfetcher, please do not modify it manually.
-    { fetchgit, fetchurl }:
+    { fetchgit, fetchurl, fetchFromGitHub }:
     {
       $body
     }
   |]
+
+generatedNixFileName :: String
+generatedNixFileName = "generated.nix"
+
+generatedJsonFileName :: String
+generatedJsonFileName = "generated.json"
diff --git a/src/NvFetcher/Config.hs b/src/NvFetcher/Config.hs
new file mode 100644
--- /dev/null
+++ b/src/NvFetcher/Config.hs
@@ -0,0 +1,38 @@
+-- | Copyright: (c) 2021-2022 berberman
+-- SPDX-License-Identifier: MIT
+-- Maintainer: berberman <berberman@yandex.com>
+-- Stability: experimental
+-- Portability: portable
+module NvFetcher.Config where
+
+import Data.Default
+import Development.Shake
+
+-- | Nvfetcher configuration
+data Config = Config
+  { shakeConfig :: ShakeOptions,
+    buildDir :: FilePath,
+    customRules :: Rules (),
+    actionAfterBuild :: Action (),
+    actionAfterClean :: Action (),
+    retry :: Int,
+    filterRegex :: Maybe String,
+    cacheNvchecker :: Bool
+  }
+
+instance Default Config where
+  def =
+    Config
+      { shakeConfig =
+          shakeOptions
+            { shakeProgress = progressSimple,
+              shakeThreads = 0
+            },
+        buildDir = "_sources",
+        customRules = pure (),
+        actionAfterBuild = pure (),
+        actionAfterClean = pure (),
+        retry = 3,
+        filterRegex = Nothing,
+        cacheNvchecker = True
+      }
diff --git a/src/NvFetcher/Core.hs b/src/NvFetcher/Core.hs
--- a/src/NvFetcher/Core.hs
+++ b/src/NvFetcher/Core.hs
@@ -1,12 +1,9 @@
 {-# LANGUAGE DataKinds #-}
 {-# LANGUAGE LambdaCase #-}
 {-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE QuasiQuotes #-}
 {-# LANGUAGE RecordWildCards #-}
-{-# LANGUAGE TypeApplications #-}
-{-# LANGUAGE ViewPatterns #-}
 
--- | Copyright: (c) 2021 berberman
+-- | Copyright: (c) 2021-2022 berberman
 -- SPDX-License-Identifier: MIT
 -- Maintainer: berberman <berberman@yandex.com>
 -- Stability: experimental
@@ -14,26 +11,22 @@
 module NvFetcher.Core
   ( Core (..),
     coreRules,
-    generateNixSourceExpr,
+    runPackage,
   )
 where
 
 import Data.Coerce (coerce)
 import qualified Data.HashMap.Strict as HMap
-import Data.Text (Text)
 import qualified Data.Text as T
 import Development.Shake
 import Development.Shake.FilePath
 import Development.Shake.Rule
-import NeatInterpolation (trimming)
 import NvFetcher.ExtractSrc
 import NvFetcher.FetchRustGitDeps
-import NvFetcher.NixExpr
 import NvFetcher.NixFetcher
 import NvFetcher.Nvchecker
 import NvFetcher.Types
 import NvFetcher.Types.ShakeExtras
-import NvFetcher.Utils
 
 -- | The core rule of nvchecker.
 -- all rules are wired here.
@@ -43,126 +36,80 @@
   prefetchRule
   extractSrcRule
   fetchRustGitDepsRule
-  -- TODO: maybe an oracle rule is enough
-  addBuiltinRule noLint noIdentity $ \(WithPackageKey (Core, pkg)) mOld mode -> case mode of
-    RunDependenciesSame
-      | Just old <- mOld,
-        (expr, _) <- decode' @(NixExpr, Version) old ->
-        pure $ RunResult ChangedNothing old expr
-    _ ->
-      lookupPackage pkg >>= \case
-        Nothing -> fail $ "Unkown package key: " <> show pkg
-        Just
-          Package
-            { _pversion = NvcheckerQ versionSource options,
-              _ppassthru = PackagePassthru passthruMap,
-              ..
-            } -> do
-            -- it's important to always rerun
-            -- since the package definition is not tracked at all
-            alwaysRerun
-            (NvcheckerA version mOldV) <- checkVersion versionSource options pkg
-            prefetched <- prefetch $ _pfetcher version
-            shakeDir <- getShakeDir
-            -- extract src
-            appending1 <-
-              case _pextract of
-                Just (PackageExtractSrc extract) -> do
-                  result <- HMap.toList <$> extractSrcs prefetched extract
-                  T.unlines
-                    <$> sequence
-                      [ do
-                          -- write extracted files to shake dir
-                          -- and read them in nix using 'builtins.readFile'
-                          writeFile' (shakeDir </> path) (T.unpack v)
-                          pure $ toNixExpr k <> " = builtins.readFile ./" <> T.pack path <> ";"
-                        | (k, v) <- result,
-                          let path =
-                                T.unpack _pname
-                                  <> "-"
-                                  <> T.unpack (coerce version)
-                                  </> k
-                      ]
-                _ -> pure ""
-            -- cargo lock
-            appending2 <-
-              case _pcargo of
-                Just (PackageCargoFilePath lockPath) -> do
-                  (_, lockData) <- head . HMap.toList <$> extractSrc prefetched lockPath
-                  result <- fetchRustGitDeps prefetched lockPath
-                  let body = T.unlines [asString k <> " = " <> coerce (asString $ coerce v) <> ";" | (k, v) <- HMap.toList result]
-                      lockPath' =
-                        T.unpack _pname
-                          <> "-"
-                          <> T.unpack (coerce version)
-                          </> lockPath
-                      lockPathNix = "./" <> T.pack lockPath'
-                  -- similar to extract src, write lock file to shake dir
-                  writeFile' (shakeDir </> lockPath') $ T.unpack lockData
-                  pure
-                    [trimming|
-                  cargoLock = {
-                    lockFile = $lockPathNix;
-                    outputHashes = {
-                      $body
-                    };
-                  };
-                |]
-                _ -> pure ""
-            -- passthru
-            let appending3 = T.unlines [k <> " = " <> v <> ";" | (k, asString -> v) <- HMap.toList passthruMap]
-
-            -- update changelog
-            case mOldV of
-              Nothing ->
-                recordVersionChange _pname Nothing version
-              Just old
-                | old /= version ->
-                  recordVersionChange _pname (Just old) version
-              _ -> pure ()
+  addBuiltinRule noLint noIdentity $ \(WithPackageKey (Core, pkg)) _ _ -> do
+    -- it's important to always rerun
+    -- since the package definition is not tracked at all
+    alwaysRerun
+    lookupPackage pkg >>= \case
+      Nothing -> fail $ "Unkown package key: " <> show pkg
+      Just
+        Package
+          { _pversion = CheckVersion versionSource options,
+            _ppassthru = (PackagePassthru passthru),
+            ..
+          } -> do
+          _prversion@(NvcheckerResult version _mOldV _isStale) <- checkVersion versionSource options pkg
+          _prfetched <- prefetch $ _pfetcher version
+          buildDir <- getBuildDir
+          -- extract src
+          _prextract <-
+            case _pextract of
+              Just (PackageExtractSrc extract) -> do
+                result <- HMap.toList <$> extractSrcs _prfetched extract
+                Just . HMap.fromList
+                  <$> sequence
+                    [ do
+                        -- write extracted files to build dir
+                        -- and read them in nix using 'builtins.readFile'
+                        writeFile' (buildDir </> path) (T.unpack v)
+                        pure (k, T.pack path)
+                      | (k, v) <- result,
+                        let path =
+                              "./"
+                                <> T.unpack _pname
+                                <> "-"
+                                <> T.unpack (coerce version)
+                                </> k
+                    ]
+              _ -> pure Nothing
+          -- cargo locks
+          _prcargolock <-
+            case _pcargo of
+              Just (PackageCargoLockFiles lockPath) -> do
+                lockFiles <- HMap.toList <$> extractSrcs _prfetched lockPath
+                result <- parallel $
+                  flip fmap lockFiles $ \(lockPath, lockData) -> do
+                    result <- fetchRustGitDeps _prfetched lockPath
+                    let lockPath' =
+                          T.unpack _pname
+                            <> "-"
+                            <> T.unpack (coerce version)
+                            </> lockPath
+                        lockPathNix = "./" <> T.pack lockPath'
+                    -- similar to extract src, write lock file to build dir
+                    writeFile' (buildDir </> lockPath') $ T.unpack lockData
+                    pure (lockPath, (lockPathNix, result))
+                pure . Just $ HMap.fromList result
+              _ -> pure Nothing
 
-            let result = gen _pname version prefetched $ appending1 <> appending2 <> appending3
-            pure $ RunResult ChangedRecomputeDiff (encode' (result, version)) result
+          -- update changelog
+          -- always use on disk verion
+          mOldV <- getLastVersionOnDisk pkg
+          case mOldV of
+            Nothing ->
+              recordVersionChange _pname Nothing version
+            Just old
+              | old /= version ->
+                recordVersionChange _pname (Just old) version
+            _ -> pure ()
 
--- | Run the core rule.
--- Given a 'PackageKey', run "NvFetcher.Nvchecker", "NvFetcher.NixFetcher"
--- (may also run "NvFetcher.ExtractSrc" or "NvFetrcher.FetchRustGitDeps")
---
--- @
--- Package
--- { _pname = "feeluown-core",
---   _pversion = NvcheckerQ (Pypi "feeluown") def,
---   _pfetcher = pypiFetcher "feeluown",
---   _pextract = Nothing,
---   _pcargo = Nothing,
---   _ppassthru = PackagePassthru (HashMap.fromList [("a", "B")])
--- }
--- @
---
--- resulting a nix exprs snippet like:
---
--- @
--- feeluown-core = {
---     pname = "feeluown-core";
---     version = "3.7.7";
---     src = fetchurl {
---       sha256 = "06d3j39ff9znqxkhp9ly81lcgajkhg30hyqxy2809yn23xixg3x2";
---       url = "https://pypi.io/packages/source/f/feeluown/feeluown-3.7.7.tar.gz";
---     };
---     a = "B";
---   };
--- @
-generateNixSourceExpr :: PackageKey -> Action NixExpr
-generateNixSourceExpr k = apply1 $ WithPackageKey (Core, k)
+          let _prpassthru = if HMap.null passthru then Nothing else Just passthru
+              _prname = _pname
+              _prpinned = _ppinned
+          -- Since we don't save the previous result, we are not able to know if the result changes
+          -- Depending on this rule leads to RunDependenciesChanged
+          pure $ RunResult ChangedRecomputeDiff mempty PackageResult {..}
 
-gen :: PackageName -> Version -> NixFetcher Fetched -> Text -> Text
-gen name (coerce -> ver) (toNixExpr -> srcP) appending =
-  [trimming|
-  $name = {
-    pname = "$name";
-    version = "$ver";
-    src = $srcP;$appending'
-  };
-|]
-  where
-    appending' = if T.null appending then "" else "\n" <> appending
+-- | 'Core' rule take a 'PackageKey', find the corresponding 'Package', and run all needed rules to get 'PackageResult'
+runPackage :: PackageKey -> Action PackageResult
+runPackage k = apply1 $ WithPackageKey (Core, k)
diff --git a/src/NvFetcher/ExtractSrc.hs b/src/NvFetcher/ExtractSrc.hs
--- a/src/NvFetcher/ExtractSrc.hs
+++ b/src/NvFetcher/ExtractSrc.hs
@@ -4,7 +4,7 @@
 {-# LANGUAGE ScopedTypeVariables #-}
 {-# LANGUAGE ViewPatterns #-}
 
--- | Copyright: (c) 2021 berberman
+-- | Copyright: (c) 2021-2022 berberman
 -- SPDX-License-Identifier: MIT
 -- Maintainer: berberman <berberman@yandex.com>
 -- Stability: experimental
@@ -38,15 +38,18 @@
 import NvFetcher.NixExpr
 import NvFetcher.Types
 import NvFetcher.Types.ShakeExtras
+import Prettyprinter (pretty, (<+>))
 
 -- | Rules of extract source
 extractSrcRule :: Rules ()
 extractSrcRule = void $
-  addOracleCache $ \(q :: ExtractSrcQ) -> withTempFile $ \fp -> withRetries $ do
-    writeFile' fp $ T.unpack $ wrap $ toNixExpr q
-    need [fp]
+  addOracleCache $ \(q :: ExtractSrcQ) -> withTempFile $ \fp -> withRetry $ do
+    putInfo . show $ "#" <+> pretty q
+    let nixExpr = T.unpack $ wrap $ toNixExpr q
+    putVerbose $ "Generated nix expr:\n" <> nixExpr
+    writeFile' fp nixExpr
     -- TODO: Avoid using NIX_PATH
-    (CmdTime t, StdoutTrim out, CmdLine c) <- cmd Shell $ "nix-instantiate --eval --strict --json --read-write-mode -E 'let pkgs = import <nixpkgs> { }; in ((import " <> fp <> ") pkgs)'"
+    (CmdTime t, StdoutTrim out, CmdLine c) <- quietly $ cmd Shell $ "nix-instantiate --eval --strict --json --read-write-mode -E 'let pkgs = import <nixpkgs> { }; in ((import " <> fp <> ") pkgs)'"
     putVerbose $ "Finishing running " <> c <> ", took " <> show t <> "s"
     case A.decodeStrict out of
       Just x -> pure x
diff --git a/src/NvFetcher/FetchRustGitDeps.hs b/src/NvFetcher/FetchRustGitDeps.hs
--- a/src/NvFetcher/FetchRustGitDeps.hs
+++ b/src/NvFetcher/FetchRustGitDeps.hs
@@ -3,7 +3,7 @@
 {-# LANGUAGE RecordWildCards #-}
 {-# LANGUAGE ViewPatterns #-}
 
--- | Copyright: (c) 2021 berberman
+-- | Copyright: (c) 2021-2022 berberman
 -- SPDX-License-Identifier: MIT
 -- Maintainer: berberman <berberman@yandex.com>
 -- Stability: experimental
@@ -33,6 +33,7 @@
 import NvFetcher.ExtractSrc
 import NvFetcher.NixFetcher
 import NvFetcher.Types
+import Prettyprinter (pretty, (<+>))
 import Text.Parsec
 import Text.Parsec.Text
 import Toml (TomlCodec, (.=))
@@ -41,7 +42,8 @@
 -- | Rules of fetch rust git dependencies
 fetchRustGitDepsRule :: Rules ()
 fetchRustGitDepsRule = void $
-  addOracleCache $ \(FetchRustGitDepsQ fetcher lockPath) -> do
+  addOracleCache $ \key@(FetchRustGitDepsQ fetcher lockPath) -> do
+    putInfo . show $ "#" <+> pretty key
     cargoLock <- head . HMap.elems <$> extractSrc fetcher lockPath
     deps <- case Toml.decode (Toml.list rustDepCodec "package") cargoLock of
       Right r -> pure $ nubOrdOn rrawSrc r
diff --git a/src/NvFetcher/NixExpr.hs b/src/NvFetcher/NixExpr.hs
--- a/src/NvFetcher/NixExpr.hs
+++ b/src/NvFetcher/NixExpr.hs
@@ -4,9 +4,10 @@
 {-# LANGUAGE OverloadedStrings #-}
 {-# LANGUAGE QuasiQuotes #-}
 {-# LANGUAGE RecordWildCards #-}
+{-# LANGUAGE ScopedTypeVariables #-}
 {-# LANGUAGE ViewPatterns #-}
 
--- | Copyright: (c) 2021 berberman
+-- | Copyright: (c) 2021-2022 berberman
 -- SPDX-License-Identifier: MIT
 -- Maintainer: berberman <berberman@yandex.com>
 -- Stability: experimental
@@ -21,12 +22,15 @@
 where
 
 import Data.Coerce (coerce)
+import Data.HashMap.Strict (HashMap)
+import qualified Data.HashMap.Strict as HMap
 import qualified Data.List.NonEmpty as NE
+import Data.Maybe (fromMaybe)
 import Data.Text (Text)
 import qualified Data.Text as T
 import NeatInterpolation (trimming)
 import NvFetcher.Types
-import NvFetcher.Utils (asString)
+import NvFetcher.Utils (quote, quoteIfNeeds)
 
 -- | Types can be converted into nix expr
 class ToNixExpr a where
@@ -36,7 +40,7 @@
   toNixExpr = nixFetcher "lib.fakeSha256"
 
 instance ToNixExpr (NixFetcher Fetched) where
-  toNixExpr f = nixFetcher (asString $ coerce $ _sha256 f) f
+  toNixExpr f = nixFetcher (quote $ coerce $ _sha256 f) f
 
 instance ToNixExpr Bool where
   toNixExpr True = "true"
@@ -61,11 +65,12 @@
 nixFetcher sha256 = \case
   FetchGit
     { _sha256 = _,
-      _rev = asString . toNixExpr -> rev,
+      _rev = quote . toNixExpr -> rev,
       _fetchSubmodules = toNixExpr -> fetchSubmodules,
       _deepClone = toNixExpr -> deepClone,
       _leaveDotGit = toNixExpr -> leaveDotGit,
-      _furl = asString -> url
+      _furl = quote -> url,
+      _name = nameField -> n
     } ->
       [trimming|
           fetchgit {
@@ -73,17 +78,61 @@
             rev = $rev;
             fetchSubmodules = $fetchSubmodules;
             deepClone = $deepClone;
-            leaveDotGit = $leaveDotGit;
+            leaveDotGit = $leaveDotGit;$n
             sha256 = $sha256;
           }
     |]
-  (FetchUrl (asString -> url) _) ->
+  FetchGitHub
+    { _sha256 = _,
+      _rev = quote . toNixExpr -> rev,
+      _fetchSubmodules = toNixExpr -> fetchSubmodules,
+      _deepClone = toNixExpr -> deepClone,
+      _leaveDotGit = toNixExpr -> leaveDotGit,
+      _fowner = quote -> owner,
+      _frepo = quote -> repo,
+      _name = nameField -> n
+    } ->
+      -- TODO: fix fetchFromGitHub in Nixpkgs so that deepClone and
+      -- leaveDotGit won't get passed to fetchzip
+      if (deepClone == "true") || (leaveDotGit == "true")
+        then
+          [trimming|
+               fetchFromGitHub ({
+                 owner = $owner;
+                 repo = $repo;
+                 rev = $rev;
+                 fetchSubmodules = $fetchSubmodules;
+                 deepClone = $deepClone;
+                 leaveDotGit = $leaveDotGit;$n
+                 sha256 = $sha256;
+               })
+         |]
+        else
+          [trimming|
+               fetchFromGitHub ({
+                 owner = $owner;
+                 repo = $repo;
+                 rev = $rev;
+                 fetchSubmodules = $fetchSubmodules;$n
+                 sha256 = $sha256;
+               })
+         |]
+  (FetchUrl (quote -> url) (nameField -> n) _) ->
     [trimming|
           fetchurl {
+            url = $url;$n
+            sha256 = $sha256;
+          }
+    |]
+  (FetchTarball (quote -> url) _) ->
+    [trimming|
+          fetchTarball {
             url = $url;
             sha256 = $sha256;
           }
     |]
+  where
+    nameField = maybe "" (\x -> "\nname = " <> quote x <> ";")
 
 instance ToNixExpr ExtractSrcQ where
   toNixExpr (ExtractSrcQ fetcher files) = extractFiles fetcher files
@@ -104,3 +153,80 @@
       value = toFile x;
     }) fileNames)
   |]
+
+-- | nix expr snippet like:
+--
+-- @
+-- feeluown-core = {
+--     pname = "feeluown-core";
+--     version = "3.7.7";
+--     src = fetchurl {
+--       sha256 = "06d3j39ff9znqxkhp9ly81lcgajkhg30hyqxy2809yn23xixg3x2";
+--       url = "https://pypi.io/packages/source/f/feeluown/feeluown-3.7.7.tar.gz";
+--     };
+--     a = "B";
+--   };
+-- @
+instance ToNixExpr PackageResult where
+  toNixExpr PackageResult {..} =
+    [trimming|
+        $name = {
+          pname = $nameString;
+          version = $version;
+          src = $src;$appending
+        };
+    |]
+    where
+      name = quoteIfNeeds _prname
+      nameString = quote _prname
+      version = quote . coerce . nvNow $ _prversion
+      src = toNixExpr _prfetched
+      extract =
+        maybe
+          ""
+          ( \ex ->
+              T.unlines
+                [ quoteIfNeeds (T.pack name)
+                    <> " = builtins.readFile "
+                    <> fp
+                    <> ";"
+                  | (name, fp) <- HMap.toList ex
+                ]
+          )
+          _prextract
+      cargo = fromMaybe "" $ do
+        cargoLocks <- _prcargolock
+        let depsSnippet (deps :: HashMap Text Checksum) =
+              T.unlines
+                [ quoteIfNeeds name
+                    <> " = "
+                    <> quote (coerce sum)
+                    <> ";"
+                  | (name, sum) <- HMap.toList deps
+                ]
+            lockSnippet ((T.pack -> fp) :: FilePath, (nixFP :: NixExpr, deps :: HashMap Text Checksum)) =
+              let hashes = depsSnippet deps
+               in [trimming|
+                    cargoLock."$fp" = {
+                      lockFile = $nixFP;
+                      outputHashes = {
+                        $hashes
+                      };
+                    };
+                |]
+        pure . T.unlines $ lockSnippet <$> HMap.toList cargoLocks
+      passthru =
+        maybe
+          ""
+          ( \pt ->
+              T.unlines
+                [ quoteIfNeeds k
+                    <> " = "
+                    <> v
+                    <> ";"
+                  | (k, quote -> v) <- HMap.toList pt
+                ]
+          )
+          _prpassthru
+      joined = extract <> cargo <> passthru
+      appending = if T.null joined then "" else "\n" <> joined
diff --git a/src/NvFetcher/NixFetcher.hs b/src/NvFetcher/NixFetcher.hs
--- a/src/NvFetcher/NixFetcher.hs
+++ b/src/NvFetcher/NixFetcher.hs
@@ -8,7 +8,7 @@
 {-# LANGUAGE TypeApplications #-}
 {-# LANGUAGE ViewPatterns #-}
 
--- | Copyright: (c) 2021 berberman
+-- | Copyright: (c) 2021-2022 berberman
 -- SPDX-License-Identifier: MIT
 -- Maintainer: berberman <berberman@yandex.com>
 -- Stability: experimental
@@ -16,10 +16,11 @@
 --
 -- 'NixFetcher' is used to describe how to fetch package sources.
 --
--- There are two types of fetchers overall:
+-- There are three types of fetchers overall:
 --
--- 1. 'FetchGit' -- nix-prefetch-git
--- 2. 'FetchUrl' -- nix-prefetch-url
+-- 1. 'FetchGit' -- nix-prefetch fetchgit
+-- 2. 'FetchGitHub' -- nix-prefetch fetchFromGitHub
+-- 3. 'FetchUrl' -- nix-prefetch fetchurl
 --
 -- As you can see the type signature of 'prefetch':
 -- a fetcher will be filled with the fetch result (hash) after the prefetch.
@@ -41,12 +42,11 @@
     urlFetcher,
     openVsxFetcher,
     vscodeMarketplaceFetcher,
+    tarballFetcher,
   )
 where
 
-import Control.Monad (void, (<=<))
-import qualified Data.Aeson as A
-import qualified Data.Aeson.Types as A
+import Control.Monad (void)
 import Data.Coerce (coerce)
 import Data.Text (Text)
 import qualified Data.Text as T
@@ -55,31 +55,57 @@
 import NeatInterpolation (trimming)
 import NvFetcher.Types
 import NvFetcher.Types.ShakeExtras
+import Prettyprinter (pretty, (<+>))
 
 --------------------------------------------------------------------------------
 
 runFetcher :: NixFetcher Fresh -> Action Checksum
 runFetcher = \case
   FetchGit {..} -> do
-    let parser = A.withObject "nix-prefetch-git" $ \o -> Checksum <$> o A..: "sha256"
-    (CmdTime t, Stdout out, CmdLine c) <-
-      command [EchoStderr False] "nix-prefetch-git" $
-        [T.unpack _furl]
-          <> ["--rev", T.unpack $ coerce _rev]
-          <> ["--fetch-submodules" | _fetchSubmodules]
-          <> ["--deepClone" | _deepClone]
-          <> ["--leave-dotGit" | _leaveDotGit]
+    (CmdTime t, Stdout (T.decodeUtf8 -> out), CmdLine c) <-
+      quietly $
+        command [EchoStderr False] "nix-prefetch" $
+          ["fetchgit"]
+            <> ["--url", T.unpack _furl]
+            <> ["--rev", T.unpack $ coerce _rev]
+            <> ["--fetchSubmodules" | _fetchSubmodules]
+            <> ["--deepClone" | _deepClone]
+            <> ["--leaveDotGit" | _leaveDotGit]
     putVerbose $ "Finishing running " <> c <> ", took " <> show t <> "s"
-    let result = A.parseMaybe parser <=< A.decodeStrict $ out
-    case result of
-      Just x -> pure x
-      _ -> fail $ "Failed to parse output from nix-prefetch-git: " <> T.unpack (T.decodeUtf8 out)
+    case takeWhile (not . T.null) $ reverse $ T.lines out of
+      [x] -> pure $ coerce x
+      _ -> fail $ "Failed to parse output from nix-prefetch: " <> T.unpack out
+  FetchGitHub {..} -> do
+    (CmdTime t, Stdout (T.decodeUtf8 -> out), CmdLine c) <-
+      quietly $
+        command [EchoStderr False] "nix-prefetch" $
+          ["fetchFromGitHub"]
+            <> ["--owner", T.unpack _fowner]
+            <> ["--repo", T.unpack _frepo]
+            <> ["--rev", T.unpack $ coerce _rev]
+            <> ["--fetchSubmodules" | _fetchSubmodules]
+            <> ["--deepClone" | _deepClone]
+            <> ["--leaveDotGit" | _leaveDotGit]
+    putVerbose $ "Finishing running " <> c <> ", took " <> show t <> "s"
+    case takeWhile (not . T.null) $ reverse $ T.lines out of
+      [x] -> pure $ coerce x
+      _ -> fail $ "Failed to parse output from nix-prefetch: " <> T.unpack out
   FetchUrl {..} -> do
-    (CmdTime t, Stdout (T.decodeUtf8 -> out), CmdLine c) <- command [EchoStderr False] "nix-prefetch-url" [T.unpack _furl]
+    (CmdTime t, Stdout (T.decodeUtf8 -> out), CmdLine c) <-
+      quietly $
+        command [EchoStderr False] "nix-prefetch" ["fetchurl", "--url", T.unpack _furl]
     putVerbose $ "Finishing running " <> c <> ", took " <> show t <> "s"
     case takeWhile (not . T.null) $ reverse $ T.lines out of
       [x] -> pure $ coerce x
-      _ -> fail $ "Failed to parse output from nix-prefetch-url: " <> T.unpack out
+      _ -> fail $ "Failed to parse output from nix-prefetch: " <> T.unpack out
+  FetchTarball {..} -> do
+    (CmdTime t, Stdout (T.decodeUtf8 -> out), CmdLine c) <-
+      quietly $
+        command [EchoStderr False] "nix-prefetch" ["fetchTarball", "--url", T.unpack _furl]
+    putVerbose $ "Finishing running " <> c <> ", took " <> show t <> "s"
+    case takeWhile (not . T.null) $ reverse $ T.lines out of
+      [x] -> pure $ coerce x
+      _ -> fail $ "Failed to parse output from nix-prefetch: " <> T.unpack out
 
 pypiUrl :: Text -> Version -> Text
 pypiUrl pypi (coerce -> ver) =
@@ -92,7 +118,8 @@
 prefetchRule :: Rules ()
 prefetchRule = void $
   addOracleCache $ \(f :: NixFetcher Fresh) -> do
-    sha256 <- withRetries $ runFetcher f
+    putInfo . show $ "#" <+> pretty f
+    sha256 <- withRetry $ runFetcher f
     pure $ f {_sha256 = sha256}
 
 -- | Run nix fetcher
@@ -103,14 +130,14 @@
 
 -- | Create a fetcher from git url
 gitFetcher :: Text -> PackageFetcher
-gitFetcher furl rev = FetchGit furl rev False False False ()
+gitFetcher furl rev = FetchGit furl rev False False False Nothing ()
 
 -- | Create a fetcher from github repo
 gitHubFetcher ::
   -- | owner and repo
   (Text, Text) ->
   PackageFetcher
-gitHubFetcher (owner, repo) = gitFetcher [trimming|https://github.com/$owner/$repo|]
+gitHubFetcher (owner, repo) rev = FetchGitHub owner repo rev False False False Nothing ()
 
 -- | Create a fetcher from pypi
 pypiFetcher :: Text -> PackageFetcher
@@ -129,7 +156,7 @@
 
 -- | Create a fetcher from url
 urlFetcher :: Text -> NixFetcher Fresh
-urlFetcher = flip FetchUrl ()
+urlFetcher url = FetchUrl url Nothing ()
 
 -- | Create a fetcher from openvsx
 openVsxFetcher ::
@@ -137,8 +164,10 @@
   (Text, Text) ->
   PackageFetcher
 openVsxFetcher (publisher, extName) (coerce -> ver) =
-  urlFetcher
+  FetchUrl
     [trimming|https://open-vsx.org/api/$publisher/$extName/$ver/file/$publisher.$extName-$ver.vsix|]
+    (Just [trimming|$extName-$ver.zip|])
+    ()
 
 -- | Create a fetcher from vscode marketplace
 vscodeMarketplaceFetcher ::
@@ -146,5 +175,11 @@
   (Text, Text) ->
   PackageFetcher
 vscodeMarketplaceFetcher (publisher, extName) (coerce -> ver) =
-  urlFetcher
+  FetchUrl
     [trimming|https://$publisher.gallery.vsassets.io/_apis/public/gallery/publisher/$publisher/extension/$extName/$ver/assetbyname/Microsoft.VisualStudio.Services.VSIXPackage|]
+    (Just [trimming|$extName-$ver.zip|])
+    ()
+
+-- | Create a fetcher from url, using fetchTarball
+tarballFetcher :: Text -> NixFetcher Fresh
+tarballFetcher url = FetchTarball url ()
diff --git a/src/NvFetcher/Nvchecker.hs b/src/NvFetcher/Nvchecker.hs
--- a/src/NvFetcher/Nvchecker.hs
+++ b/src/NvFetcher/Nvchecker.hs
@@ -1,9 +1,8 @@
 {-# LANGUAGE LambdaCase #-}
 {-# LANGUAGE OverloadedStrings #-}
 {-# LANGUAGE RecordWildCards #-}
-{-# LANGUAGE ViewPatterns #-}
 
--- | Copyright: (c) 2021 berberman
+-- | Copyright: (c) 2021-2022 berberman
 -- SPDX-License-Identifier: MIT
 -- Maintainer: berberman <berberman@yandex.com>
 -- Stability: experimental
@@ -14,26 +13,29 @@
 -- Now we call nvchecker for each 'VersionSource', which seems not to be efficient, but it's tolerable when running in parallel.
 --
 -- Meanwhile, we lose the capabilities of tracking version updates, i.e. normally nvchecker will help us maintain a list of old versions,
--- so that we are able to know which package's version is updated in this run. Fortunately, we can reimplement this using shake database,
+-- so that we are able to know which package's version is updated in this run. Fortunately, we can reimplement this in shake,
 -- see 'nvcheckerRule' for details.
 module NvFetcher.Nvchecker
   ( -- * Types
     VersionSortMethod (..),
     ListOptions (..),
-    NvcheckerQ (..),
+    CheckVersion (..),
     NvcheckerOptions (..),
     VersionSource (..),
-    NvcheckerA (..),
+    NvcheckerResult (..),
 
     -- * Rules
     nvcheckerRule,
     checkVersion,
+    checkVersion',
   )
 where
 
+import Control.Monad (void)
+import Control.Monad.Extra (fromMaybeM)
 import qualified Data.Aeson as A
 import Data.Coerce (coerce)
-import Data.Maybe (mapMaybe)
+import Data.Maybe (fromJust, mapMaybe)
 import Data.String (fromString)
 import qualified Data.Text as T
 import qualified Data.Text.Encoding as T
@@ -42,41 +44,75 @@
 import NvFetcher.Types
 import NvFetcher.Types.ShakeExtras
 import NvFetcher.Utils
-import Toml (Value (Bool, Text), pretty)
+import Prettyprinter (pretty, (<+>))
+import Toml (Value (Bool, Text))
+import qualified Toml
 import Toml.Type.Edsl
 
 -- | Rules of nvchecker
 nvcheckerRule :: Rules ()
-nvcheckerRule = addBuiltinRule noLint noIdentity $ \(WithPackageKey (NvcheckerQ versionSource options, pkg)) old _mode ->
-  -- If the package was removed after the last run,
-  -- shake still runs the nvchecker rule for this package.
-  -- So we record a version change here, indicating that the package has been removed.
-  -- Ideally, this should be done in the core rule
-  isPackageKeyTarget pkg >>= \case
-    False -> do
-      let oldVer = decode' <$> old
-      recordVersionChange (coerce pkg) oldVer "∅"
-      pure $ RunResult ChangedRecomputeDiff mempty undefined -- skip running, returning a never consumed result
-    _ ->
-      withTempFile $ \config -> withRetries $ do
-        writeFile' config $ T.unpack $ pretty $ mkToml $ genNvConfig pkg options versionSource
-        need [config]
-        (CmdTime t, Stdout out, CmdLine c) <- cmd $ "nvchecker --logger json -c " <> config
-        putVerbose $ "Finishing running " <> c <> ", took " <> show t <> "s"
-        let out' = T.decodeUtf8 out
-            result = mapMaybe (A.decodeStrict . T.encodeUtf8) (T.lines out')
-        now <- case result of
-          [x] -> pure x
-          _ -> fail $ "Failed to parse output from nvchecker: " <> T.unpack out'
-        pure $ case old of
-          Just lastRun
-            | cachedResult <- decode' lastRun ->
-              if cachedResult == nvNow now
-                then -- try to get the version in last run from store, filling it into 'now'
-                  RunResult ChangedRecomputeSame lastRun now {nvOld = Just cachedResult}
-                else RunResult ChangedRecomputeDiff (encode' $ nvNow now) now {nvOld = Just cachedResult}
-          Nothing -> RunResult ChangedRecomputeDiff (encode' $ nvNow now) now
+nvcheckerRule = do
+  persistedRule
+  oneShotRule
 
+-- | Nvchecker rule for packages, which is aware of version changes and supports using stale version.
+-- nvchecker will be called at most one time given a package key. Follow-up using of this rule will return cached result.
+-- 'PackageKey' is required for caching.
+-- Run this rule by calling 'checkVersion'
+persistedRule :: Rules ()
+persistedRule = addBuiltinRule noLint noIdentity $ \(WithPackageKey (key@(CheckVersion versionSource options), pkg)) _old _mode -> do
+  putInfo . show $ "#" <+> pretty key
+  oldVer <- getRecentLastVersion pkg
+  useStaleVersion <- _ppinned . fromJust <$> lookupPackage pkg
+  let useStale = case useStaleVersion of
+        PermanentStale -> True
+        TemporaryStale -> True
+        _ -> False
+  case useStale of
+    True
+      | Just oldVer' <- oldVer -> do
+        -- use the stale version if we have
+        putInfo $ T.unpack $ "Skip running nvchecker, use stale version " <> coerce oldVer' <> " for " <> coerce pkg
+        let result = NvcheckerResult {nvNow = oldVer', nvOld = oldVer, nvStale = True}
+        pure $ RunResult ChangedRecomputeSame (encode' result) result
+
+    -- run nvchecker
+    _ -> do
+      -- if we already run this rule for a package, we can recover the last result from getLastVersionUpdated
+      -- (when cacheNvchecker is enabled)
+      useCache <- nvcheckerCacheEnabled
+      now <- fromMaybeM (coerce <$> runNvchecker pkg options versionSource) (if useCache then getLastVersionUpdated pkg else pure Nothing)
+      let runChanged = case oldVer of
+            Just oldVer'
+              | oldVer' == now -> ChangedRecomputeSame
+            _ -> ChangedRecomputeDiff
+          result = NvcheckerResult {nvNow = now, nvOld = oldVer, nvStale = False}
+      -- always update
+      updateLastVersion pkg now
+      pure $ RunResult runChanged mempty result
+
+-- | Nvchecker rule without cache
+-- Rule this rule by calling 'checkVersion''
+oneShotRule :: Rules ()
+oneShotRule = void $
+  addOracle $ \key@(CheckVersion versionSource options) -> do
+    putInfo . show $ pretty key
+    NvcheckerRaw now <- runNvchecker (PackageKey "pkg") options versionSource
+    pure $ NvcheckerResult now Nothing False
+
+runNvchecker :: PackageKey -> NvcheckerOptions -> VersionSource -> Action NvcheckerRaw
+runNvchecker pkg options versionSource = withTempFile $ \config -> withRetry $ do
+  let nvcheckerConfig = T.unpack $ Toml.pretty $ mkToml $ genNvConfig pkg options versionSource
+  putVerbose $ "Generated nvchecker config for " <> show pkg <> ":" <> nvcheckerConfig
+  writeFile' config nvcheckerConfig
+  (CmdTime t, Stdout out, CmdLine c) <- quietly . cmd $ "nvchecker --logger json -c " <> config
+  putVerbose $ "Finishing running " <> c <> ", took " <> show t <> "s"
+  let out' = T.decodeUtf8 out
+      result = mapMaybe (A.decodeStrict . T.encodeUtf8) (T.lines out')
+  case result of
+    [x] -> pure x
+    _ -> fail $ "Failed to parse output from nvchecker: " <> T.unpack out'
+
 genNvConfig :: PackageKey -> NvcheckerOptions -> VersionSource -> TDSL
 genNvConfig pkg options versionSource = table (fromString $ T.unpack $ coerce pkg) $ do
   genVersionSource versionSource
@@ -133,6 +169,9 @@
       VscodeMarketplace {..} -> do
         "source" =: "vsmarketplace"
         "vsmarketplace" =: Text (_vsmPublisher <> "." <> _vsmExtName)
+      Cmd {..} -> do
+        "source" =: "cmd"
+        "cmd" =: Text _vcmd
     genListOptions ListOptions {..} = do
       "include_regex" =:? _includeRegex
       "exclude_regex" =:? _excludeRegex
@@ -143,6 +182,11 @@
       "from_pattern" =:? _fromPattern
       "to_pattern" =:? _toPattern
 
--- | Run nvchecker
-checkVersion :: VersionSource -> NvcheckerOptions -> PackageKey -> Action NvcheckerA
-checkVersion v o k = apply1 $ WithPackageKey (NvcheckerQ v o, k)
+-- | Run nvchecker given 'PackageKey'
+-- Recording version changes and using stale version are available.
+checkVersion :: VersionSource -> NvcheckerOptions -> PackageKey -> Action NvcheckerResult
+checkVersion v o k = apply1 $ WithPackageKey (CheckVersion v o, k)
+
+-- | Run nvchecker without cache
+checkVersion' :: VersionSource -> NvcheckerOptions -> Action NvcheckerResult
+checkVersion' v o = askOracle $ CheckVersion v o
diff --git a/src/NvFetcher/Options.hs b/src/NvFetcher/Options.hs
--- a/src/NvFetcher/Options.hs
+++ b/src/NvFetcher/Options.hs
@@ -1,6 +1,7 @@
+{-# LANGUAGE LambdaCase #-}
 {-# LANGUAGE TemplateHaskell #-}
 
--- | Copyright: (c) 2021 berberman
+-- | Copyright: (c) 2021-2022 berberman
 -- SPDX-License-Identifier: MIT
 -- Maintainer: berberman <berberman@yandex.com>
 -- Stability: experimental
@@ -9,6 +10,7 @@
 -- CLI interface of nvfetcher
 module NvFetcher.Options
   ( CLIOptions (..),
+    Target (..),
     cliOptionsParser,
     getCLIOptions,
   )
@@ -17,16 +19,30 @@
 import Options.Applicative.Simple
 import qualified Paths_nvfetcher as Paths
 
+data Target = Build | Clean
+  deriving (Eq)
+
+instance Show Target where
+  show Build = "build"
+  show Clean = "clean"
+
+targetParser :: ReadM Target
+targetParser = maybeReader $ \case
+  "build" -> Just Build
+  "clean" -> Just Clean
+  _ -> Nothing
+
 -- | Options for nvfetcher CLI
 data CLIOptions = CLIOptions
-  { buildDir :: FilePath,
-    commit :: Bool,
-    logPath :: Maybe FilePath,
-    threads :: Int,
-    retries :: Int,
-    timing :: Bool,
-    verbose :: Bool,
-    target :: String
+  { optBuildDir :: FilePath,
+    optCommit :: Bool,
+    optLogPath :: Maybe FilePath,
+    optThreads :: Int,
+    optRetry :: Int,
+    optTiming :: Bool,
+    optVerbose :: Bool,
+    optPkgNameFilter :: Maybe String,
+    optTarget :: Target
   }
   deriving (Show)
 
@@ -44,7 +60,7 @@
       )
     <*> switch
       ( long "commit-changes"
-          <> help "`git commit` changes in this run (with shake db)"
+          <> help "`git commit` build dir with version changes as commit message"
       )
     <*> optional
       ( strOption
@@ -74,11 +90,20 @@
       )
     <*> switch (long "timing" <> short 't' <> help "Show build time")
     <*> switch (long "verbose" <> short 'v' <> help "Verbose mode")
-    <*> strArgument
+    <*> optional
+      ( strOption
+          ( short 'f'
+              <> long "filter"
+              <> metavar "REGEX"
+              <> help "Regex to filter packages to be updated"
+          )
+      )
+    <*> argument
+      targetParser
       ( metavar "TARGET"
           <> help "Two targets are available: 1.build  2.clean"
-          <> value "build"
-          <> completer (listCompleter ["build", "clean"])
+          <> value Build
+          <> completer (listCompleter [show Build, show Clean])
           <> showDefault
       )
 
diff --git a/src/NvFetcher/PackageSet.hs b/src/NvFetcher/PackageSet.hs
--- a/src/NvFetcher/PackageSet.hs
+++ b/src/NvFetcher/PackageSet.hs
@@ -1,9 +1,7 @@
 {-# LANGUAGE DataKinds #-}
-{-# LANGUAGE DeriveFunctor #-}
 {-# LANGUAGE FlexibleContexts #-}
 {-# LANGUAGE FlexibleInstances #-}
 {-# LANGUAGE GADTs #-}
-{-# LANGUAGE KindSignatures #-}
 {-# LANGUAGE LambdaCase #-}
 {-# LANGUAGE MultiParamTypeClasses #-}
 {-# LANGUAGE OverloadedStrings #-}
@@ -15,7 +13,7 @@
 {-# LANGUAGE UndecidableInstances #-}
 {-# LANGUAGE ViewPatterns #-}
 
--- | Copyright: (c) 2021 berberman
+-- | Copyright: (c) 2021-2022 berberman
 -- SPDX-License-Identifier: MIT
 -- Maintainer: berberman <berberman@yandex.com>
 -- Stability: experimental
@@ -66,6 +64,7 @@
     sourceHttpHeader,
     sourceOpenVsx,
     sourceVscodeMarketplace,
+    sourceCmd,
 
     -- ** Fetchers
     fetchGitHub,
@@ -77,12 +76,14 @@
     fetchUrl,
     fetchOpenVsx,
     fetchVscodeMarketplace,
+    fetchTarball,
 
     -- * Addons
     extractSource,
-    hasCargoLock,
+    hasCargoLocks,
     tweakVersion,
     passthru,
+    pinned,
 
     -- ** Miscellaneous
     Prod,
@@ -139,7 +140,7 @@
 -- 1. Carry defined packages
 -- 2. Run IO actions
 --
--- Package set is evaluated be for shake runs.
+-- Package set is evaluated before shake runs.
 -- Use 'newPackage' to add a new package, 'liftIO' to run an IO action.
 type PackageSet = Free PackageSetF
 
@@ -149,13 +150,15 @@
 -- | Add a package to package set
 newPackage ::
   PackageName ->
-  NvcheckerQ ->
+  CheckVersion ->
   PackageFetcher ->
   Maybe PackageExtractSrc ->
-  Maybe PackageCargoFilePath ->
+  Maybe PackageCargoLockFiles ->
   PackagePassthru ->
+  UseStaleVersion ->
   PackageSet ()
-newPackage name source fetcher extract cargo pasthru = liftF $ NewPackage (Package name source fetcher extract cargo pasthru) ()
+newPackage name source fetcher extract cargo pasthru useStale =
+  liftF $ NewPackage (Package name source fetcher extract cargo pasthru useStale) ()
 
 -- | Add a list of packages into package set
 purePackageSet :: [Package] -> PackageSet ()
@@ -250,9 +253,10 @@
         r,
       OptionalMembers
         '[ PackageExtractSrc,
-           PackageCargoFilePath,
+           PackageCargoLockFiles,
            NvcheckerOptions,
-           PackagePassthru
+           PackagePassthru,
+           UseStaleVersion
          ]
         r
     ) =>
@@ -271,11 +275,12 @@
     p <- e
     newPackage
       (proj p)
-      (NvcheckerQ (proj p) (fromMaybe def (projMaybe p)))
+      (CheckVersion (proj p) (fromMaybe def (projMaybe p)))
       (proj p)
       (projMaybe p)
       (projMaybe p)
       (fromMaybe mempty (projMaybe p))
+      (fromMaybe NoStale (projMaybe p))
 
 -- | 'PkgDSL' version of 'newPackage'
 --
@@ -293,9 +298,10 @@
       r,
     OptionalMembers
       '[ PackageExtractSrc,
-         PackageCargoFilePath,
+         PackageCargoLockFiles,
          PackagePassthru,
-         NvcheckerOptions
+         NvcheckerOptions,
+         UseStaleVersion
        ]
       r
   ) =>
@@ -436,6 +442,12 @@
 sourceVscodeMarketplace :: Attach VersionSource (Text, Text)
 sourceVscodeMarketplace e (_vsmPublisher, _vsmExtName) = src e VscodeMarketplace {..}
 
+-- | This package follows a version from a shell command
+--
+-- Arg is the command to run
+sourceCmd :: Attach VersionSource Text
+sourceCmd e _vcmd = src e Cmd {..}
+
 --------------------------------------------------------------------------------
 
 -- | This package is fetched from a github repo
@@ -450,7 +462,7 @@
 -- For example, you can enable fetch submodules like:
 --
 -- @
--- define $ package "qliveplayer" `sourceGitHub` ("IsoaSFlus", "QLivePlayer") `fetchGitHub'` ("IsoaSFlus", "QLivePlayer", fetchSubmodules .~ True)
+-- define $ package "qliveplayer" `sourceGitHub` ("THMonster", "QLivePlayer") `fetchGitHub'` ("THMonster", "QLivePlayer", fetchSubmodules .~ True)
 -- @
 fetchGitHub' :: Attach PackageFetcher (Text, Text, NixFetcher Fresh -> NixFetcher Fresh)
 fetchGitHub' e (owner, repo, f) = fetch e $ f . gitHubFetcher (owner, repo)
@@ -498,17 +510,23 @@
 fetchVscodeMarketplace :: Attach PackageFetcher (Text, Text)
 fetchVscodeMarketplace e = fetch e . vscodeMarketplaceFetcher
 
+-- | This package is a tarball, fetched from url
+--
+-- Arg is a function which constructs the url from a version
+fetchTarball :: Attach PackageFetcher (Version -> Text)
+fetchTarball e f = fetch e (tarballFetcher . f)
+
 --------------------------------------------------------------------------------
 
 -- | Extract files from fetched package source
 extractSource :: Attach PackageExtractSrc [FilePath]
 extractSource = (. pure . PackageExtractSrc . NE.fromList) . andThen
 
--- | Run 'FetchRustGitDependencies' given the path to @Cargo.lock@
+-- | Run 'FetchRustGitDependencies' given the path to @Cargo.lock@ files
 --
--- The lock file will be extracted as well.
-hasCargoLock :: Attach PackageCargoFilePath FilePath
-hasCargoLock = (. pure . PackageCargoFilePath) . andThen
+-- The lock files will be extracted as well.
+hasCargoLocks :: Attach PackageCargoLockFiles [FilePath]
+hasCargoLocks = (. pure . PackageCargoLockFiles . NE.fromList) . andThen
 
 -- | Set 'NvcheckerOptions' for a package, which can tweak the version number we obtain
 tweakVersion :: Attach NvcheckerOptions (NvcheckerOptions -> NvcheckerOptions)
@@ -519,3 +537,9 @@
 -- Arg is a list of kv pairs
 passthru :: Attach PackagePassthru [(Text, Text)]
 passthru = (. pure . PackagePassthru . HMap.fromList) . andThen
+
+-- | Pin a package
+--
+-- new version won't be checked if we have a stale version
+pinned :: PackageSet (Prod r) -> PackageSet (Prod (UseStaleVersion : r))
+pinned = flip andThen . pure $ PermanentStale
diff --git a/src/NvFetcher/Types.hs b/src/NvFetcher/Types.hs
--- a/src/NvFetcher/Types.hs
+++ b/src/NvFetcher/Types.hs
@@ -3,17 +3,16 @@
 {-# LANGUAGE DeriveGeneric #-}
 {-# LANGUAGE DerivingStrategies #-}
 {-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE FlexibleInstances #-}
 {-# LANGUAGE GeneralizedNewtypeDeriving #-}
 {-# LANGUAGE LambdaCase #-}
 {-# LANGUAGE OverloadedStrings #-}
 {-# LANGUAGE RecordWildCards #-}
 {-# LANGUAGE StandaloneDeriving #-}
-{-# LANGUAGE TypeApplications #-}
 {-# LANGUAGE TypeFamilies #-}
 {-# LANGUAGE UndecidableInstances #-}
-{-# LANGUAGE ViewPatterns #-}
 
--- | Copyright: (c) 2021 berberman
+-- | Copyright: (c) 2021-2022 berberman
 -- SPDX-License-Identifier: MIT
 -- Maintainer: berberman <berberman@yandex.com>
 -- Stability: experimental
@@ -33,9 +32,11 @@
     VersionSortMethod (..),
     ListOptions (..),
     VersionSource (..),
-    NvcheckerA (..),
-    NvcheckerQ (..),
+    NvcheckerResult (..),
+    NvcheckerRaw (..),
+    CheckVersion (..),
     NvcheckerOptions (..),
+    UseStaleVersion (..),
 
     -- * Nix fetcher types
     NixFetcher (..),
@@ -55,10 +56,11 @@
     PackageName,
     PackageFetcher,
     PackageExtractSrc (..),
-    PackageCargoFilePath (..),
+    PackageCargoLockFiles (..),
     PackagePassthru (..),
     Package (..),
     PackageKey (..),
+    PackageResult (..),
   )
 where
 
@@ -67,31 +69,32 @@
 import Data.Default
 import Data.HashMap.Strict (HashMap)
 import qualified Data.List.NonEmpty as NE
-import Data.Maybe (fromMaybe)
+import Data.Maybe (fromMaybe, isNothing)
 import Data.String (IsString)
 import Data.Text (Text)
 import qualified Data.Text as T
 import Development.Shake
 import Development.Shake.Classes
 import GHC.Generics (Generic)
+import Prettyprinter
 
 --------------------------------------------------------------------------------
 
 -- | Package version
 newtype Version = Version Text
-  deriving newtype (Eq, Show, Ord, IsString, Semigroup, Monoid, A.FromJSON, A.ToJSON)
+  deriving newtype (Eq, Show, Ord, IsString, Semigroup, Monoid, A.FromJSON, A.ToJSON, Pretty)
   deriving stock (Typeable, Generic)
   deriving anyclass (Hashable, Binary, NFData)
 
 -- | Check sum, sha256, sri or base32, etc.
 newtype Checksum = Checksum Text
-  deriving newtype (Show, Eq, Ord)
+  deriving newtype (Show, Eq, Ord, A.FromJSON, A.ToJSON, Pretty)
   deriving stock (Typeable, Generic)
   deriving anyclass (Hashable, Binary, NFData)
 
 -- | Git branch ('Nothing': master)
 newtype Branch = Branch (Maybe Text)
-  deriving newtype (Show, Eq, Ord, Default)
+  deriving newtype (Show, Eq, Ord, Default, Pretty)
   deriving stock (Typeable, Generic)
   deriving anyclass (Hashable, Binary, NFData)
 
@@ -126,6 +129,10 @@
     ParseVersion -> "parse_version"
     Vercmp -> "vercmp"
 
+instance Pretty VersionSortMethod where
+  pretty ParseVersion = "ParseVersion"
+  pretty Vercmp = "Vercmp"
+
 instance Default VersionSortMethod where
   def = ParseVersion
 
@@ -139,6 +146,27 @@
   }
   deriving (Show, Typeable, Eq, Ord, Generic, Hashable, Binary, NFData, Default)
 
+isEmptyListOptions :: ListOptions -> Bool
+isEmptyListOptions ListOptions {..} =
+  isNothing _includeRegex
+    && isNothing _excludeRegex
+    && isNothing _sortVersionKey
+    && isNothing _includeRegex
+
+instance Pretty ListOptions where
+  pretty ListOptions {..} =
+    "ListOptions"
+      <> indent
+        2
+        ( vsep $
+            concat
+              [ ppField "includeRegex" _includeRegex,
+                ppField "excludeRegex" _excludeRegex,
+                ppField "sortVersionKey" _sortVersionKey,
+                ppField "ignored" _includeRegex
+              ]
+        )
+
 -- | Configuration available for evey version sourece.
 -- See <https://nvchecker.readthedocs.io/en/latest/usage.html#global-options> for details.
 data NvcheckerOptions = NvcheckerOptions
@@ -148,6 +176,30 @@
   }
   deriving (Show, Typeable, Eq, Ord, Generic, Hashable, Binary, NFData, Default)
 
+isEmptyNvcheckerOptions :: NvcheckerOptions -> Bool
+isEmptyNvcheckerOptions NvcheckerOptions {..} =
+  isNothing _stripPrefix
+    && isNothing _fromPattern
+    && isNothing _toPattern
+
+instance Pretty NvcheckerOptions where
+  pretty NvcheckerOptions {..} =
+    "NvcheckerOptions"
+      <> line
+      <> indent
+        2
+        ( vsep $
+            concat
+              [ ppField "stripPrefix" _stripPrefix,
+                ppField "fromPattern" _fromPattern,
+                ppField "toPattern" _toPattern
+              ]
+        )
+
+ppField :: Pretty a => Doc ann -> Maybe a -> [Doc ann]
+ppField _ Nothing = []
+ppField s (Just x) = [s <> colon <+> pretty x]
+
 -- | Upstream version source for nvchecker to check
 data VersionSource
   = GitHubRelease {_owner :: Text, _repo :: Text}
@@ -162,26 +214,133 @@
   | HttpHeader {_vurl :: Text, _regex :: Text, _listOptions :: ListOptions}
   | OpenVsx {_ovPublisher :: Text, _ovExtName :: Text}
   | VscodeMarketplace {_vsmPublisher :: Text, _vsmExtName :: Text}
+  | Cmd {_vcmd :: Text}
   deriving (Show, Typeable, Eq, Ord, Generic, Hashable, Binary, NFData)
 
+instance Pretty VersionSource where
+  pretty GitHubRelease {..} =
+    "CheckGitHubRelease"
+      <> line
+      <> indent
+        2
+        ( vsep
+            [ "owner" <> colon <+> pretty _owner,
+              "repo" <> colon <+> pretty _repo
+            ]
+        )
+  pretty GitHubTag {..} =
+    "CheckGitHubTag"
+      <> line
+      <> indent
+        2
+        ( vsep $
+            [ "owner" <> colon <+> pretty _owner,
+              "repo" <> colon <+> pretty _repo
+            ]
+              <> ["listOptions" <> colon <+> pretty _listOptions | not $ isEmptyListOptions _listOptions]
+        )
+  pretty Git {..} =
+    "CheckGit"
+      <> line
+      <> indent
+        2
+        ( vsep
+            [ "url" <> colon <+> pretty _vurl,
+              "branch" <> colon <+> pretty _vbranch
+            ]
+        )
+  pretty Pypi {..} =
+    "CheckPypi" <> colon <+> pretty _pypi
+  pretty ArchLinux {..} =
+    "CheckArchLinux" <> colon <+> pretty _archpkg
+  pretty Aur {..} =
+    "CheckAur" <> colon <+> pretty _aur
+  pretty Manual {..} =
+    "CheckManual" <> colon <+> pretty _manual
+  pretty Repology {..} =
+    "CheckRepology"
+      <> line
+      <> indent
+        2
+        ( vsep
+            [ "repology" <> colon <+> pretty _repology,
+              "repo" <> colon <+> pretty _repo
+            ]
+        )
+  pretty Webpage {..} =
+    "CheckWebpage"
+      <> line
+      <> indent
+        2
+        ( vsep $
+            [ "url" <> colon <+> pretty _vurl,
+              "regex" <> colon <+> pretty _regex
+            ]
+              <> ["listOptions" <> colon <+> pretty _listOptions | not $ isEmptyListOptions _listOptions]
+        )
+  pretty HttpHeader {..} =
+    "CheckHttpHeader"
+      <> line
+      <> indent
+        2
+        ( vsep $
+            [ "url" <> colon <+> pretty _vurl,
+              "regex" <> colon <+> pretty _regex
+            ]
+              <> ["listOptions" <> colon <+> pretty _listOptions | not $ isEmptyListOptions _listOptions]
+        )
+  pretty OpenVsx {..} =
+    "CheckOpenVsx"
+      <> line
+      <> indent
+        2
+        ( vsep
+            [ "publisher" <> colon <+> pretty _ovPublisher,
+              "extName" <> colon <+> pretty _ovExtName
+            ]
+        )
+  pretty VscodeMarketplace {..} =
+    "CheckVscodeMarketplace"
+      <> line
+      <> indent
+        2
+        ( vsep
+            [ "publisher" <> colon <+> pretty _vsmPublisher,
+              "extName" <> colon <+> pretty _vsmExtName
+            ]
+        )
+  pretty Cmd {..} =
+    "CheckCmd" <> colon <+> pretty _vcmd
+
 -- | The input of nvchecker
-data NvcheckerQ = NvcheckerQ VersionSource NvcheckerOptions
+data CheckVersion = CheckVersion VersionSource NvcheckerOptions
   deriving (Show, Typeable, Eq, Ord, Generic, Hashable, Binary, NFData)
 
--- | The result of running nvchecker
-data NvcheckerA = NvcheckerA
+instance Pretty CheckVersion where
+  pretty (CheckVersion v n) = align (vsep $ [pretty v] <> [pretty n | not $ isEmptyNvcheckerOptions n])
+
+-- | The result of nvchecker rule
+data NvcheckerResult = NvcheckerResult
   { nvNow :: Version,
-    -- | nvchecker doesn't give this value, but shake restores it from last run
-    nvOld :: Maybe Version
+    -- | last result of this nvchecker rule
+    -- TODO: consider removing this field
+    nvOld :: Maybe Version,
+    -- | stale means even 'nvNow' comes from json file (last run)
+    -- and we actually didn't run nvchecker this time. 'nvOld' will be 'Nothing' in this case.
+    nvStale :: Bool
   }
   deriving (Show, Typeable, Eq, Generic, Hashable, Binary, NFData)
 
-instance A.FromJSON NvcheckerA where
-  parseJSON = A.withObject "NvcheckerResult" $ \o ->
-    NvcheckerA <$> o A..: "version" <*> pure Nothing
+-- | Parsed JSON output from nvchecker
+newtype NvcheckerRaw = NvcheckerRaw Version
+  deriving (Show, Typeable, Eq, Generic)
 
-type instance RuleResult NvcheckerQ = NvcheckerA
+instance A.FromJSON NvcheckerRaw where
+  parseJSON = A.withObject "NvcheckerRaw" $ \o ->
+    NvcheckerRaw <$> o A..: "version"
 
+type instance RuleResult CheckVersion = NvcheckerResult
+
 --------------------------------------------------------------------------------
 
 -- | If the package is prefetched, then we can obtain the SHA256
@@ -192,9 +351,28 @@
         _deepClone :: Bool,
         _fetchSubmodules :: Bool,
         _leaveDotGit :: Bool,
+        _name :: Maybe Text,
         _sha256 :: FetchResult k
       }
-  | FetchUrl {_furl :: Text, _sha256 :: FetchResult k}
+  | FetchGitHub
+      { _fowner :: Text,
+        _frepo :: Text,
+        _rev :: Version,
+        _deepClone :: Bool,
+        _fetchSubmodules :: Bool,
+        _leaveDotGit :: Bool,
+        _name :: Maybe Text,
+        _sha256 :: FetchResult k
+      }
+  | FetchUrl
+      { _furl :: Text,
+        _name :: Maybe Text,
+        _sha256 :: FetchResult k
+      }
+  | FetchTarball
+      { _furl :: Text,
+        _sha256 :: FetchResult k
+      }
   deriving (Typeable, Generic)
 
 -- | Fetch status
@@ -219,6 +397,86 @@
 
 deriving instance NFData (FetchResult k) => NFData (NixFetcher k)
 
+instance A.ToJSON (NixFetcher Fetched) where
+  toJSON FetchGit {..} =
+    A.object
+      [ "url" A..= _furl,
+        "rev" A..= _rev,
+        "deepClone" A..= _deepClone,
+        "fetchSubmodules" A..= _fetchSubmodules,
+        "leaveDotGit" A..= _leaveDotGit,
+        "name" A..= _name,
+        "sha256" A..= _sha256,
+        "type" A..= A.String "git"
+      ]
+  toJSON FetchGitHub {..} =
+    A.object
+      [ "owner" A..= _fowner,
+        "repo" A..= _frepo,
+        "rev" A..= _rev,
+        "deepClone" A..= _deepClone,
+        "fetchSubmodules" A..= _fetchSubmodules,
+        "leaveDotGit" A..= _leaveDotGit,
+        "name" A..= _name,
+        "sha256" A..= _sha256,
+        "type" A..= A.String "github"
+      ]
+  toJSON FetchUrl {..} =
+    A.object
+      [ "url" A..= _furl,
+        "name" A..= _name,
+        "sha256" A..= _sha256,
+        "type" A..= A.String "url"
+      ]
+  toJSON FetchTarball {..} =
+    A.object
+      [ "url" A..= _furl,
+        "sha256" A..= _sha256,
+        "type" A..= A.String "tarball"
+      ]
+
+instance Pretty (NixFetcher k) where
+  pretty FetchGit {..} =
+    "FetchGit"
+      <> line
+      <> indent
+        2
+        ( vsep $
+            [ "url" <> colon <+> pretty _furl,
+              "rev" <> colon <+> pretty _rev,
+              "deepClone" <> colon <+> pretty _deepClone,
+              "fetchSubmodules" <> colon <+> pretty _fetchSubmodules,
+              "leaveDotGit" <> colon <+> pretty _leaveDotGit
+            ]
+              <> ppField "name" _name
+        )
+  pretty FetchGitHub {..} =
+    "FetchGitHub"
+      <> line
+      <> indent
+        2
+        ( vsep $
+            [ "owner" <> colon <+> pretty _fowner,
+              "repo" <> colon <+> pretty _frepo,
+              "rev" <> colon <+> pretty _rev,
+              "deepClone" <> colon <+> pretty _deepClone,
+              "fetchSubmodules" <> colon <+> pretty _fetchSubmodules,
+              "leaveDotGit" <> colon <+> pretty _leaveDotGit
+            ]
+              <> ppField "name" _name
+        )
+  pretty FetchUrl {..} =
+    "FetchUrl"
+      <> line
+      <> indent
+        2
+        ( vsep $
+            ["url" <> colon <+> pretty _furl]
+              <> ppField "name" _name
+        )
+  pretty FetchTarball {..} =
+    "FetchTarball" <> colon <+> pretty _furl
+
 --------------------------------------------------------------------------------
 
 -- | Extract file contents from package source
@@ -228,6 +486,17 @@
 
 type instance RuleResult ExtractSrcQ = HashMap FilePath Text
 
+instance Pretty ExtractSrcQ where
+  pretty (ExtractSrcQ f n) =
+    "ExtractSrc" <> line
+      <> indent
+        2
+        ( vsep
+            [ "fetcher" <> colon <+> pretty f,
+              "files" <> colon <+> pretty n
+            ]
+        )
+
 --------------------------------------------------------------------------------
 
 -- | Fetch @outputHashes@ for git dependencies in @Cargo.lock@.
@@ -239,6 +508,17 @@
 -- | @outputHashes@, a mapping from nameVer -> output hash
 type instance RuleResult FetchRustGitDepsQ = HashMap Text Checksum
 
+instance Pretty FetchRustGitDepsQ where
+  pretty (FetchRustGitDepsQ f n) =
+    "FetchRustGitDeps" <> line
+      <> indent
+        2
+        ( vsep
+            [ "fetcher" <> colon <+> pretty f,
+              "cargoLock" <> colon <+> pretty n
+            ]
+        )
+
 --------------------------------------------------------------------------------
 
 -- | Package name, used in generating nix expr
@@ -249,36 +529,51 @@
 
 newtype PackageExtractSrc = PackageExtractSrc (NE.NonEmpty FilePath)
 
-newtype PackageCargoFilePath = PackageCargoFilePath FilePath
+newtype PackageCargoLockFiles = PackageCargoLockFiles (NE.NonEmpty FilePath)
 
 newtype PackagePassthru = PackagePassthru (HashMap Text Text)
   deriving newtype (Semigroup, Monoid)
 
+-- | Using stale value indicates that we will /NOT/ check for new versions if
+-- there is a known version recoverd from json file or last use of the rule.
+-- Normally you don't want a stale version
+-- unless you need pin a package.
+data UseStaleVersion
+  = -- | Specified in configuration file
+    PermanentStale
+  | -- | Specified by @--filter@ command
+    TemporaryStale
+  | NoStale
+  deriving stock (Eq, Show, Ord, Typeable, Generic)
+  deriving anyclass (Hashable, Binary, NFData)
+
 -- | A package is defined with:
 --
 -- 1. its name
 -- 2. how to track its version
 -- 3. how to fetch it as we have the version
--- 4. optional file paths to extract (dump to shake dir)
+-- 4. optional file paths to extract (dump to build dir)
 -- 5. optional @Cargo.lock@ path (if it's a rust package)
 -- 6. an optional pass through map
+-- 7. if the package version was pinned
 --
 -- /INVARIANT: 'Version' passed to 'PackageFetcher' MUST be used textually,/
 -- /i.e. can only be concatenated with other strings,/
 -- /in case we can't check the equality between fetcher functions./
 data Package = Package
   { _pname :: PackageName,
-    _pversion :: NvcheckerQ,
+    _pversion :: CheckVersion,
     _pfetcher :: PackageFetcher,
     _pextract :: Maybe PackageExtractSrc,
-    _pcargo :: Maybe PackageCargoFilePath,
-    _ppassthru :: PackagePassthru
+    _pcargo :: Maybe PackageCargoLockFiles,
+    _ppassthru :: PackagePassthru,
+    _ppinned :: UseStaleVersion
   }
 
 -- | Package key is the name of a package.
 -- We use this type to index packages.
 newtype PackageKey = PackageKey PackageName
-  deriving newtype (Eq, Show, Ord)
+  deriving newtype (Eq, Show, Ord, Pretty)
   deriving stock (Typeable, Generic)
   deriving anyclass (Hashable, Binary, NFData)
 
@@ -288,7 +583,7 @@
 data Core = Core
   deriving (Eq, Show, Ord, Typeable, Generic, Hashable, Binary, NFData)
 
-type instance RuleResult Core = NixExpr
+type instance RuleResult Core = PackageResult
 
 -- | Decorate a rule's key with 'PackageKey'
 newtype WithPackageKey k = WithPackageKey (k, PackageKey)
@@ -298,3 +593,31 @@
   show (WithPackageKey (k, n)) = show k <> " (" <> show n <> ")"
 
 type instance RuleResult (WithPackageKey k) = RuleResult k
+
+-- | Result type of 'Core'
+data PackageResult = PackageResult
+  { _prname :: PackageName,
+    _prversion :: NvcheckerResult,
+    _prfetched :: NixFetcher 'Fetched,
+    _prpassthru :: Maybe (HashMap Text Text),
+    -- | extracted file name -> file path in build dir
+    _prextract :: Maybe (HashMap FilePath NixExpr),
+    -- | cargo lock file path in build dir -> (file path in nix, git dependencies)
+    _prcargolock :: Maybe (HashMap FilePath (NixExpr, HashMap Text Checksum)),
+    _prpinned :: UseStaleVersion
+  }
+  deriving (Show, Typeable, Generic, NFData)
+
+instance A.ToJSON PackageResult where
+  toJSON PackageResult {..} =
+    A.object
+      [ "name" A..= _prname,
+        "version" A..= nvNow _prversion,
+        "src" A..= _prfetched,
+        "extract" A..= _prextract,
+        "passthru" A..= _prpassthru,
+        "cargoLocks" A..= _prcargolock,
+        "pinned" A..= case _prpinned of
+          PermanentStale -> True
+          _ -> False
+      ]
diff --git a/src/NvFetcher/Types/Lens.hs b/src/NvFetcher/Types/Lens.hs
--- a/src/NvFetcher/Types/Lens.hs
+++ b/src/NvFetcher/Types/Lens.hs
@@ -1,7 +1,7 @@
 {-# LANGUAGE DataKinds #-}
 {-# LANGUAGE TemplateHaskell #-}
 
--- | Copyright: (c) 2021 berberman
+-- | Copyright: (c) 2021-2022 berberman
 -- SPDX-License-Identifier: MIT
 -- Maintainer: berberman <berberman@yandex.com>
 -- Stability: experimental
diff --git a/src/NvFetcher/Types/ShakeExtras.hs b/src/NvFetcher/Types/ShakeExtras.hs
--- a/src/NvFetcher/Types/ShakeExtras.hs
+++ b/src/NvFetcher/Types/ShakeExtras.hs
@@ -2,7 +2,7 @@
 {-# LANGUAGE RecordWildCards #-}
 {-# LANGUAGE TypeApplications #-}
 
--- | Copyright: (c) 2021 berberman
+-- | Copyright: (c) 2021-2022 berberman
 -- SPDX-License-Identifier: MIT
 -- Maintainer: berberman <berberman@yandex.com>
 -- Stability: experimental
@@ -24,8 +24,21 @@
     recordVersionChange,
     getVersionChanges,
 
-    -- * Retries
-    withRetries,
+    -- * Retry
+    withRetry,
+
+    -- * Build dir
+    getBuildDir,
+
+    -- * Last versions
+    getLastVersionOnDisk,
+    getRecentLastVersion,
+    updateLastVersion,
+    getAllOnDiskVersions,
+    getLastVersionUpdated,
+
+    -- * Cache nvchecker
+    nvcheckerCacheEnabled,
   )
 where
 
@@ -33,13 +46,22 @@
 import Data.Map.Strict (Map)
 import qualified Data.Map.Strict as Map
 import Development.Shake
+import NvFetcher.Config
 import NvFetcher.Types
 
+data LastVersion
+  = OnDisk Version
+  | Updated
+      (Maybe Version)
+      -- ^ on disk if has
+      Version
+
 -- | Values we use during the build. It's stored in 'shakeExtra'
 data ShakeExtras = ShakeExtras
-  { versionChanges :: Var [VersionChange],
+  { config :: Config,
+    versionChanges :: Var [VersionChange],
     targetPackages :: Map PackageKey Package,
-    retries :: Int
+    lastVersions :: Var (Map PackageKey LastVersion)
   }
 
 -- | Get our values from shake
@@ -49,10 +71,12 @@
     Just x -> pure x
     _ -> fail "ShakeExtras is missing!"
 
--- | Create an empty 'ShakeExtras' from packages to build and times to retry for each rule
-initShakeExtras :: Map PackageKey Package -> Int -> IO ShakeExtras
-initShakeExtras targetPackages retries = do
+-- | Create an empty 'ShakeExtras' from packages to build, times to retry for each rule,
+-- build dir, and on disk versions
+initShakeExtras :: Config -> Map PackageKey Package -> Map PackageKey Version -> IO ShakeExtras
+initShakeExtras config targetPackages lv = do
   versionChanges <- newVar mempty
+  lastVersions <- newVar $ Map.map OnDisk lv
   pure ShakeExtras {..}
 
 -- | Get keys of all packages to build
@@ -83,6 +107,64 @@
   ShakeExtras {..} <- getShakeExtras
   liftIO $ readVar versionChanges
 
--- | Run an action, retry at most 'retries' times if it throws an exception
-withRetries :: Action a -> Action a
-withRetries a = getShakeExtras >>= \ShakeExtras {..} -> actionRetry retries a
+-- | Run an action, retry at most 'retry' times (defined in config) if it throws an exception
+withRetry :: Action a -> Action a
+withRetry a = getShakeExtras >>= \ShakeExtras {..} -> actionRetry (retry config) a
+
+-- | Get build dir
+getBuildDir :: Action FilePath
+getBuildDir = buildDir . config <$> getShakeExtras
+
+-- | Get initial version of a package
+getLastVersionOnDisk :: PackageKey -> Action (Maybe Version)
+getLastVersionOnDisk k = do
+  ShakeExtras {..} <- getShakeExtras
+  versions <- liftIO $ readVar lastVersions
+  pure $ case versions Map.!? k of
+    Just (Updated v _) -> v
+    Just (OnDisk v) -> Just v
+    _ -> Nothing
+
+-- | Get version of a package, no matter it was initial one or rule result
+getRecentLastVersion :: PackageKey -> Action (Maybe Version)
+getRecentLastVersion k = do
+  ShakeExtras {..} <- getShakeExtras
+  versions <- liftIO $ readVar lastVersions
+  pure $ case versions Map.!? k of
+    Just (Updated _ v) -> Just v
+    Just (OnDisk v) -> Just v
+    _ -> Nothing
+
+-- | Get updated version of a package
+getLastVersionUpdated :: PackageKey -> Action (Maybe Version)
+getLastVersionUpdated k = do
+  ShakeExtras {..} <- getShakeExtras
+  versions <- liftIO $ readVar lastVersions
+  pure $ case versions Map.!? k of
+    Just (Updated _ v) -> Just v
+    _ -> Nothing
+
+-- | Add nvchecker result of a package
+updateLastVersion :: PackageKey -> Version -> Action ()
+updateLastVersion k v = do
+  ShakeExtras {..} <- getShakeExtras
+  liftIO $
+    modifyVar_ lastVersions $ \versions -> pure $ case versions Map.!? k of
+      Just (Updated o _) -> Map.insert k (Updated o v) versions
+      Just (OnDisk lv) -> Map.insert k (Updated (Just lv) v) versions
+      _ -> Map.insert k (Updated Nothing v) versions
+
+-- | Get all initial versions
+getAllOnDiskVersions :: Action (Map PackageKey Version)
+getAllOnDiskVersions = do
+  ShakeExtras {..} <- getShakeExtras
+  versions <- liftIO $ readVar lastVersions
+  let xs = Map.toList $
+        flip Map.map versions $ \case
+          OnDisk v -> Just v
+          Updated v _ -> v
+  pure $ Map.fromList [(k, v) | (k, Just v) <- xs]
+
+-- | Get if 'cacheNvchecker' is enabled
+nvcheckerCacheEnabled :: Action Bool
+nvcheckerCacheEnabled = cacheNvchecker . config <$> getShakeExtras
diff --git a/src/NvFetcher/Utils.hs b/src/NvFetcher/Utils.hs
--- a/src/NvFetcher/Utils.hs
+++ b/src/NvFetcher/Utils.hs
@@ -1,3 +1,10 @@
+{-# LANGUAGE CPP #-}
+
+-- | Copyright: (c) 2021-2022 berberman
+-- SPDX-License-Identifier: MIT
+-- Maintainer: berberman <berberman@yandex.com>
+-- Stability: experimental
+-- Portability: portable
 module NvFetcher.Utils where
 
 import Data.Binary
@@ -5,7 +12,11 @@
 import qualified Data.ByteString.Lazy as LBS
 import Data.Text (Text)
 import qualified Data.Text as T
-import Development.Shake
+import System.Directory.Extra (XdgDirectory (XdgData), getXdgDirectory)
+import Text.Regex.TDFA ((=~))
+#if MIN_VERSION_aeson(2,0,0)
+import qualified Data.Aeson.Key as A
+#endif
 
 encode' :: Binary a => a -> BS.ByteString
 encode' = BS.concat . LBS.toChunks . encode
@@ -13,8 +24,24 @@
 decode' :: Binary a => BS.ByteString -> a
 decode' = decode . LBS.fromChunks . pure
 
-asString :: Text -> Text
-asString = T.pack . show
+quote :: Text -> Text
+quote = T.pack . show
 
-getShakeDir :: Action FilePath
-getShakeDir = shakeFiles <$> getShakeOptions
+isLegalNixId :: Text -> Bool
+isLegalNixId x = x =~ "^[a-zA-Z_][a-zA-Z0-9_'-]*$"
+
+quoteIfNeeds :: Text -> Text
+quoteIfNeeds x
+  | isLegalNixId x = x
+  | otherwise = quote x
+
+getDataDir :: IO FilePath
+getDataDir = getXdgDirectory XdgData "nvfetcher"
+
+#if MIN_VERSION_aeson(2,0,0)
+aesonKey :: Text -> A.Key
+aesonKey = A.fromText
+#else
+aesonKey :: Text -> Text
+aesonKey = id
+#endif
diff --git a/test/CheckVersionSpec.hs b/test/CheckVersionSpec.hs
--- a/test/CheckVersionSpec.hs
+++ b/test/CheckVersionSpec.hs
@@ -2,20 +2,29 @@
 
 module CheckVersionSpec where
 
+import Control.Monad.IO.Class (liftIO)
 import Control.Monad.Trans.Reader
 import Data.Coerce (coerce)
 import Data.Default (def)
 import qualified Data.Map.Strict as Map
+import qualified Data.Text as T
 import Lens.Micro
+import NvFetcher.Config (Config (cacheNvchecker))
 import NvFetcher.Nvchecker
 import NvFetcher.Types
 import NvFetcher.Types.Lens
+import System.IO.Extra (newTempFile)
 import Test.Hspec
 import Utils
 
--- | We need a fakePackageKey here; otherwise the nvchecker rule would be cutoff
 spec :: Spec
-spec = aroundShake' (Map.singleton fakePackageKey fakePackage) $
+spec = do
+  versionSourcesSpec
+  useStaleSpec
+  cacheSpec
+
+versionSourcesSpec :: Spec
+versionSourcesSpec = aroundShake $
   describe "nvchecker" $ do
     specifyChan "pypi" $
       runNvcheckerRule (Pypi "example") `shouldReturnJust` Version "0.1.0"
@@ -43,10 +52,14 @@
       runNvcheckerRule (GitHubTag "harry-sanabria" "ReleaseTestRepo" $ def & ignored ?~ "second_release release3")
         `shouldReturnJust` Version "first_release"
 
-    specifyChan "http header" $ do
+    specifyChan "http header" $
       runNvcheckerRule (HttpHeader "https://www.unifiedremote.com/download/linux-x64-deb" "urserver-([\\d.]+).deb" def)
         >>= shouldBeJust
 
+    -- specifyChan "webpage" $
+    --   runNvcheckerRule (Webpage "http://ftp.vim.org/pub/vim/patches/7.3/" "7\\.3\\.\\d+" def)
+    --     `shouldReturnJust` Version "7.3.1314"
+
     specifyChan "manual" $
       runNvcheckerRule (Manual "Meow") `shouldReturnJust` Version "Meow"
 
@@ -59,11 +72,54 @@
     specifyChan "vsmarketplace" $
       runNvcheckerRule (VscodeMarketplace "usernamehw" "indent-one-space") `shouldReturnJust` Version "0.2.8"
 
+    specifyChan "cmd" $
+      runNvcheckerRule (Cmd "echo Meow") `shouldReturnJust` Version "Meow"
+
 --------------------------------------------------------------------------------
 
+-- | We need disable nvchecker cache to see if use stale works
+useStaleSpec :: Spec
+useStaleSpec = aroundShake' (Map.singleton fakePackageKey fakePinnedPackage) def {cacheNvchecker = False} $
+  describe "useStale" $ do
+    (temp, cleanup) <- runIO newTempFile
+
+    let versionSource = Cmd $ "cat " <> T.pack temp
+
+    specifyChan "needs run" $ do
+      liftIO $ writeFile temp "Meow"
+      runNvcheckerRuleOnFakePackage versionSource `shouldReturnJust` NvcheckerResult {nvNow = "Meow", nvOld = Nothing, nvStale = False}
+
+    specifyChan "stale" $ do
+      liftIO $ writeFile temp "Bark"
+      runNvcheckerRuleOnFakePackage versionSource `shouldReturnJust` NvcheckerResult {nvNow = "Meow", nvOld = Just "Meow", nvStale = True}
+
+    runIO cleanup
+
+cacheSpec :: Spec
+cacheSpec = aroundShake' (Map.singleton fakePackageKey fakePackage) def {cacheNvchecker = True} $
+  describe "cache" $ do
+    (temp, cleanup) <- runIO newTempFile
+
+    let versionSource = Cmd $ "cat " <> T.pack temp
+
+    specifyChan "needs run" $ do
+      liftIO $ writeFile temp "Meow"
+      runNvcheckerRuleOnFakePackage versionSource `shouldReturnJust` NvcheckerResult {nvNow = "Meow", nvOld = Nothing, nvStale = False}
+
+    specifyChan "cache" $ do
+      liftIO $ writeFile temp "Bark"
+      runNvcheckerRuleOnFakePackage versionSource `shouldReturnJust` NvcheckerResult {nvNow = "Meow", nvOld = Just "Meow", nvStale = False}
+
+    runIO cleanup
+
+--------------------------------------------------------------------------------
+
 runNvcheckerRule :: VersionSource -> ReaderT ActionQueue IO (Maybe Version)
-runNvcheckerRule v = runActionChan $ nvNow <$> checkVersion v def fakePackageKey
+runNvcheckerRule v = fmap nvNow <$> runActionChan (checkVersion' v def)
 
+runNvcheckerRuleOnFakePackage :: VersionSource -> ReaderT ActionQueue IO (Maybe NvcheckerResult)
+runNvcheckerRuleOnFakePackage v = runActionChan $ checkVersion v def fakePackageKey
+
 fakePackageKey :: PackageKey
 fakePackageKey = PackageKey "a-fake-package"
 
@@ -75,5 +131,9 @@
       _pfetcher = undefined,
       _pcargo = undefined,
       _pextract = undefined,
-      _ppassthru = undefined
+      _ppassthru = undefined,
+      _ppinned = NoStale
     }
+
+fakePinnedPackage :: Package
+fakePinnedPackage = fakePackage {_ppinned = PermanentStale}
diff --git a/test/FetchRustGitDepsSpec.hs b/test/FetchRustGitDepsSpec.hs
--- a/test/FetchRustGitDepsSpec.hs
+++ b/test/FetchRustGitDepsSpec.hs
@@ -26,7 +26,7 @@
       shouldBeJust prefetched
       runFetchRustGitDepsRule (fromJust prefetched) "Cargo.lock"
         `shouldReturnJust` HMap.fromList
-          [ ("rand-0.8.3", Checksum "1khg0rnz8xxd389cprqmy9vq9sggzz78lb9n7hh2w6xfsl4xfyyc")
+          [ ("rand-0.8.3", Checksum "sha256-zHvXCdWuGy4gPDYtis7/7+mEd/IV58sSGq139G0GD84=")
           ]
 
 runPrefetchRule :: NixFetcher Fresh -> ReaderT ActionQueue IO (Maybe (NixFetcher Fetched))
diff --git a/test/NixExprSpec.hs b/test/NixExprSpec.hs
--- a/test/NixExprSpec.hs
+++ b/test/NixExprSpec.hs
@@ -35,6 +35,18 @@
       }
     |]
 
+  it "renders fresh gitHubFetcher" $
+    toNixExpr (gitHubFetcher ("owner", "repo") "fake_rev")
+      `shouldBe` [trimming|
+      fetchFromGitHub ({
+        owner = "owner";
+        repo = "repo";
+        rev = "fake_rev";
+        fetchSubmodules = false;
+        sha256 = lib.fakeSha256;
+      })
+    |]
+
   it "renders fresh urlFetcher" $
     toNixExpr (urlFetcher "https://example.com")
       `shouldBe` [trimming|
@@ -44,10 +56,20 @@
       }
     |]
 
+  it "renders filename for vsc extension" $
+    toNixExpr (openVsxFetcher ("publisher", "extension") "fake_version")
+      `shouldBe` [trimming|
+      fetchurl {
+        url = "https://open-vsx.org/api/publisher/extension/fake_version/file/publisher.extension-fake_version.vsix";
+        name = "extension-fake_version.zip";
+        sha256 = lib.fakeSha256;
+      }
+    |]
+
   it "renders IFD of ExtractSrcQ" $
     toNixExpr
       ( ExtractSrcQ
-          (FetchUrl "https://example.com" (Checksum "calculated sha256"))
+          (FetchUrl "https://example.com" Nothing (Checksum "calculated sha256"))
           (NE.fromList ["a.txt", "b.txt"])
       )
       `shouldBe` [trimming|
diff --git a/test/PrefetchSpec.hs b/test/PrefetchSpec.hs
--- a/test/PrefetchSpec.hs
+++ b/test/PrefetchSpec.hs
@@ -14,23 +14,27 @@
   describe "fetchers" $ do
     specifyChan "pypi" $
       runPrefetchRule (pypiFetcher "example" "0.1.0")
-        `shouldReturnJust` Checksum "1fy3zylvks372jz7fkgad3x14bscz19m2anlq2bn8xs1r1p3x1zm"
+        `shouldReturnJust` Checksum "sha256-9Yc+bshBd2SXwNQqUVP4TC8S+mjqTXe+FGfouan/w7s="
 
     specifyChan "openvsx" $
       runPrefetchRule (openVsxFetcher ("usernamehw", "indent-one-space") "0.2.6")
-        `shouldReturnJust` Checksum "0smb75z10h1aiqmj2irn2vz1z99djfsfpkczwiqk447frx388bd1"
+        `shouldReturnJust` Checksum "sha256-oS2ERs/uEDJx5J/N67STLaUf/hY2RyErjipAEH45q2o="
 
     specifyChan "vsmarketplace" $
       runPrefetchRule (vscodeMarketplaceFetcher ("usernamehw", "indent-one-space") "0.2.6")
-        `shouldReturnJust` Checksum "1vmq24hdbv8jwhvy85s7qq9gffcsndvsw8vmphxs95r7bc3439w7"
+        `shouldReturnJust` Checksum "sha256-h6dBBlsnl6Q7vHUjrnezmjn3EsZHF+Q35BLt1SARuO4="
 
     specifyChan "git" $
-      runPrefetchRule (gitFetcher "https://gitlab.com/gitlab-org/gitlab-test.git" "ddd0f15ae83993f5cb66a927a28673882e99100b")
-        `shouldReturnJust` Checksum "10d0yqa0h00pd5a36nwx3hb4apd4f9hki0pi545ffgrpf9nzzg92"
+      runPrefetchRule (gitFetcher "https://github.com/git-up/test-repo-submodules" "5a1dfa807759c39e3df891b6b46dfb2cf776c6ef")
+        `shouldReturnJust` Checksum "sha256-wCo1YobyatxSOE85xQNSJw6jvufghFNHlZl4ToQjRHA="
 
     specifyChan "github" $
       runPrefetchRule (gitHubFetcher ("harry-sanabria", "ReleaseTestRepo") "release3")
-        `shouldReturnJust` Checksum "1a8qv2h9ji6w5p4ljwwkgvk49w6hj9j7hgmw0ahw10y1i45s0b3i"
+        `shouldReturnJust` Checksum "sha256-cSygC4nBg8ChArw+eGSS0PBE5n6Tc0nJLdxEmaDYGKk="
+
+    specifyChan "tarball" $
+      runPrefetchRule (tarballFetcher "https://github.com/nixos/nixpkgs/archive/3d35529a48d3ad50ad959463755b0b7fe392cfa7.tar.gz")
+        `shouldReturnJust` Checksum "0la68sv52zz1kjw5s5sn6qslzz9mi9sakhzwi873gp4dhc8df1sg"
 
 --------------------------------------------------------------------------------
 
diff --git a/test/Utils.hs b/test/Utils.hs
--- a/test/Utils.hs
+++ b/test/Utils.hs
@@ -12,10 +12,12 @@
 import Control.Monad (void)
 import Control.Monad.IO.Class
 import Control.Monad.Trans.Reader
+import Data.Default (def)
 import Data.Map (Map)
 import Data.Maybe (isJust)
 import Development.Shake
 import Development.Shake.Database
+import NvFetcher.Config
 import NvFetcher.Core (coreRules)
 import NvFetcher.Types
 import NvFetcher.Types.ShakeExtras
@@ -35,9 +37,9 @@
 
 type ActionQueue = TQueue (Action ())
 
-newAsyncActionQueue :: Map PackageKey Package -> IO (ActionQueue, Async ())
-newAsyncActionQueue pkgs = Extra.withTempDir $ \dir -> do
-  shakeExtras <- liftIO $ initShakeExtras pkgs 3
+newAsyncActionQueue :: Map PackageKey Package -> Config -> IO (ActionQueue, Async ())
+newAsyncActionQueue pkgs config = Extra.withTempDir $ \dir -> do
+  shakeExtras <- liftIO $ initShakeExtras config {buildDir = dir} pkgs mempty
   chan <- atomically newTQueue
   (getShakeDb, _) <-
     shakeOpenDatabase
@@ -65,12 +67,12 @@
 --------------------------------------------------------------------------------
 
 aroundShake :: SpecWith ActionQueue -> Spec
-aroundShake = aroundShake' mempty
+aroundShake = aroundShake' mempty def
 
-aroundShake' :: Map PackageKey Package -> SpecWith ActionQueue -> Spec
-aroundShake' pkgs = aroundAll $ \f ->
+aroundShake' :: Map PackageKey Package -> Config -> SpecWith ActionQueue -> Spec
+aroundShake' pkgs config = aroundAll $ \f ->
   bracket
-    (newAsyncActionQueue pkgs)
+    (newAsyncActionQueue pkgs config)
     (\(_, runnerTask) -> cancel runnerTask)
     (\(chan, _) -> f chan)
 
