diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,27 @@
 
 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 
+## 0.2.0.0 - 2021-06-16
+
+### Added
+
+ - Speculative support for nixUnstable
+
+ - `StorePath` (compatible for stable Nix)
+
+ - Use and build on inline-c-cpp's TemplateHaskell-based template support
+
+ - `nixVersion`, `storeDir`
+
+ - Some settings getters
+
+### Changed
+
+ - Many functions now work with `StorePath` instead of plain paths
+
+ - Some functions now need a `Store` because of the `StorePath` change
+
+
 ## 0.1.1.0 - 2021-04-21
 
 ### Added
diff --git a/compat-2.3/derivation-output.cxx b/compat-2.3/derivation-output.cxx
new file mode 100644
--- /dev/null
+++ b/compat-2.3/derivation-output.cxx
@@ -0,0 +1,23 @@
+
+#include <nix/config.h>
+#include <nix/store-api.hh>
+#include <nix/derivations.hh>
+#include <nix/path-compat.hh>
+#include "nix/derivation-output.hh"
+
+using namespace nix;
+
+static std::string outputPathName(std::string_view drvName, std::string_view outputName) {
+    std::string res { drvName };
+    if (outputName != "out") {
+        res += "-";
+        res += outputName;
+    }
+    return res;
+}
+
+StorePath DerivationOutputCAFixed::path(const Store & store, std::string_view drvName, std::string_view outputName) const {
+    return parseStorePath(store, store.makeFixedOutputPath(
+        hash.method == FileIngestionMethod::Recursive, hash.hash,
+        outputPathName(drvName, outputName)));
+}
diff --git a/compat-2.3/include/nix/content-address.hh b/compat-2.3/include/nix/content-address.hh
new file mode 100644
--- /dev/null
+++ b/compat-2.3/include/nix/content-address.hh
@@ -0,0 +1,77 @@
+#pragma once
+
+#include <variant>
+#include "hash.hh"
+
+namespace nix {
+
+enum struct FileIngestionMethod : uint8_t {
+    Flat = false,
+    Recursive = true
+};
+
+struct TextHash {
+    Hash hash;
+};
+
+/// Pair of a hash, and how the file system was ingested
+struct FixedOutputHash {
+    FileIngestionMethod method;
+    Hash hash;
+    std::string printMethodAlgo() const;
+};
+
+/*
+  We've accumulated several types of content-addressed paths over the years;
+  fixed-output derivations support multiple hash algorithms and serialisation
+  methods (flat file vs NAR). Thus, ‘ca’ has one of the following forms:
+
+  * ‘text:sha256:<sha256 hash of file contents>’: For paths
+    computed by makeTextPath() / addTextToStore().
+
+  * ‘fixed:<r?>:<ht>:<h>’: For paths computed by
+    makeFixedOutputPath() / addToStore().
+*/
+typedef std::variant<
+    TextHash, // for paths computed by makeTextPath() / addTextToStore
+    FixedOutputHash // for path computed by makeFixedOutputPath
+> ContentAddress;
+
+/* Compute the prefix to the hash algorithm which indicates how the files were
+   ingested. */
+std::string makeFileIngestionPrefix(const FileIngestionMethod m);
+
+/* Compute the content-addressability assertion (ValidPathInfo::ca)
+   for paths created by makeFixedOutputPath() / addToStore(). */
+std::string makeFixedOutputCA(FileIngestionMethod method, const Hash & hash);
+
+std::string renderContentAddress(ContentAddress ca);
+
+std::string renderContentAddress(std::optional<ContentAddress> ca);
+
+ContentAddress parseContentAddress(std::string_view rawCa);
+
+std::optional<ContentAddress> parseContentAddressOpt(std::string_view rawCaOpt);
+
+Hash getContentAddressHash(const ContentAddress & ca);
+
+/*
+  We only have one way to hash text with references, so this is single-value
+  type is only useful in std::variant.
+*/
+struct TextHashMethod { };
+struct FixedOutputHashMethod {
+  FileIngestionMethod fileIngestionMethod;
+  HashType hashType;
+};
+
+typedef std::variant<
+    TextHashMethod,
+    FixedOutputHashMethod
+  > ContentAddressMethod;
+
+ContentAddressMethod parseContentAddressMethod(std::string_view rawCaMethod);
+
+std::string renderContentAddressMethod(ContentAddressMethod caMethod);
+
+}
diff --git a/compat-2.3/include/nix/derivation-output.hh b/compat-2.3/include/nix/derivation-output.hh
new file mode 100644
--- /dev/null
+++ b/compat-2.3/include/nix/derivation-output.hh
@@ -0,0 +1,68 @@
+#pragma once
+
+#include "path.hh"
+#include "types.hh"
+#include "hash.hh"
+#include "content-address.hh"
+#include "sync.hh"
+
+#include <map>
+#include <variant>
+
+namespace nix {
+/* Abstract syntax of derivations. */
+
+/* The traditional non-fixed-output derivation type. */
+struct DerivationOutputInputAddressed
+{
+    StorePath path;
+};
+
+/* Fixed-output derivations, whose output paths are content addressed
+   according to that fixed output. */
+struct DerivationOutputCAFixed
+{
+    std::string drvName; // !!! not in Nix 2.4, which stores name in Derivation.
+    FixedOutputHash hash; /* hash used for expected hash computation */
+    StorePath path(const Store & store, std::string_view drvName, std::string_view outputName) const;
+};
+
+/* Floating-output derivations, whose output paths are content addressed, but
+   not fixed, and so are dynamically calculated from whatever the output ends
+   up being. */
+struct DerivationOutputCAFloating
+{
+    /* information used for expected hash computation */
+    FileIngestionMethod method;
+    HashType hashType;
+};
+
+/* Input-addressed output which depends on a (CA) derivation whose hash isn't
+ * known atm
+ */
+struct DerivationOutputDeferred {};
+
+}
+
+namespace compat::nix {
+
+using namespace ::nix;
+
+struct DerivationOutput
+{
+    std::variant<
+        DerivationOutputInputAddressed,
+        DerivationOutputCAFixed,
+        DerivationOutputCAFloating,
+        DerivationOutputDeferred
+    > output;
+
+    /* Note, when you use this function you should make sure that you're passing
+       the right derivation name. When in doubt, you should use the safer
+       interface provided by BasicDerivation::outputsAndOptPaths */
+    std::optional<StorePath> path(const Store & store, std::string_view drvName, std::string_view outputName) const;
+};
+
+typedef std::map<string, DerivationOutput> DerivationOutputs;
+
+}
diff --git a/compat-2.3/include/nix/path-compat.hh b/compat-2.3/include/nix/path-compat.hh
new file mode 100644
--- /dev/null
+++ b/compat-2.3/include/nix/path-compat.hh
@@ -0,0 +1,42 @@
+#pragma once
+#include "path.hh"
+#include <vector>
+
+inline nix::Path printPath23(const nix::Store &store, const nix::StorePath &sp) {
+  return (store.storeDir + "/").append(sp.to_string());
+}
+
+inline nix::StorePath parseStorePath(const nix::Store &store, const nix::Path path) {
+  nix::Path p = nix::canonPath(path);
+  if (nix::dirOf(p) != store.storeDir)
+    throw nix::Error("path '%s' is not in the Nix store", p);
+  return nix::StorePath(nix::baseNameOf(p));
+}
+
+inline nix::PathSet printPathSet23(const nix::Store &store, std::vector<nix::StorePathWithOutputs> &sps) {
+  nix::PathSet r;
+  for (auto spwo : sps) {
+    for (auto output : spwo.outputs) {
+      r.insert(printPath23(store, spwo.path) + "!" + output);
+    }
+  }
+  return r;
+}
+
+inline nix::PathSet compatPathSet(const nix::Store &store, nix::StorePathSet &sps) {
+  nix::PathSet r;
+  for (auto sp : sps) {
+    r.insert(printPath23(store, sp));
+  }
+  return r;
+}
+
+inline nix::StorePathSet compatStorePathSet(const nix::Store &store, nix::PathSet &sps) {
+  nix::StorePathSet r;
+  for (auto sp : sps) {
+    r.insert(parseStorePath(store, sp));
+  }
+  return r;
+}
+
+#define parseStorePath23(store, path) parseStorePath(store, path)
diff --git a/compat-2.3/include/nix/path-info.hh b/compat-2.3/include/nix/path-info.hh
new file mode 100644
--- /dev/null
+++ b/compat-2.3/include/nix/path-info.hh
@@ -0,0 +1,3 @@
+#pragma once
+
+#include <nix/store-api.hh>
diff --git a/compat-2.3/include/nix/path.hh b/compat-2.3/include/nix/path.hh
new file mode 100644
--- /dev/null
+++ b/compat-2.3/include/nix/path.hh
@@ -0,0 +1,91 @@
+#pragma once
+
+// #include "content-address.hh"
+#include <nix/config.h>
+#include "types.hh"
+
+namespace nix {
+
+class Store;
+struct Hash;
+
+class StorePath
+{
+    std::string baseName;
+
+public:
+
+    /* Size of the hash part of store paths, in base-32 characters. */
+    constexpr static size_t HashLen = 32; // i.e. 160 bits
+
+    StorePath() = delete;
+
+    StorePath(std::string_view baseName);
+
+    StorePath(const Hash & hash, std::string_view name);
+
+    std::string_view to_string() const
+    {
+        return baseName;
+    }
+
+    bool operator < (const StorePath & other) const
+    {
+        return baseName < other.baseName;
+    }
+
+    bool operator == (const StorePath & other) const
+    {
+        return baseName == other.baseName;
+    }
+
+    bool operator != (const StorePath & other) const
+    {
+        return baseName != other.baseName;
+    }
+
+    /* Check whether a file name ends with the extension for
+       derivations. */
+    bool isDerivation() const;
+
+    std::string_view name() const
+    {
+        return std::string_view(baseName).substr(HashLen + 1);
+    }
+
+    std::string_view hashPart() const
+    {
+        return std::string_view(baseName).substr(0, HashLen);
+    }
+
+    static StorePath dummy;
+};
+
+typedef std::set<StorePath> StorePathSet;
+typedef std::vector<StorePath> StorePaths;
+typedef std::map<string, StorePath> OutputPathMap;
+
+// typedef std::map<StorePath, std::optional<ContentAddress>> StorePathCAMap;
+
+struct StorePathWithOutputs
+{
+    StorePath path;
+    std::set<std::string> outputs;
+
+    std::string to_string(const Store & store) const;
+};
+
+std::pair<std::string_view, StringSet> parsePathWithOutputs(std::string_view s);
+
+}
+
+namespace std {
+
+template<> struct hash<nix::StorePath> {
+    std::size_t operator()(const nix::StorePath & path) const noexcept
+    {
+        return * (std::size_t *) path.to_string().data();
+    }
+};
+
+}
diff --git a/compat-2.3/path.cxx b/compat-2.3/path.cxx
new file mode 100644
--- /dev/null
+++ b/compat-2.3/path.cxx
@@ -0,0 +1,54 @@
+
+#include <nix/config.h>
+#include <nix/store-api.hh>
+#include <nix/path-compat.hh>
+#include <nix/path.hh>
+#include <nix-compat.hh>
+
+using namespace nix;
+
+static void checkName(std::string_view path, std::string_view name)
+{
+    if (name.empty())
+        throw Error("store path '%s' has an empty name", path);
+    if (name.size() > 211)
+        throw Error("store path '%s' has a name longer than 211 characters", path);
+    for (auto c : name)
+        if (!((c >= '0' && c <= '9')
+                || (c >= 'a' && c <= 'z')
+                || (c >= 'A' && c <= 'Z')
+                || c == '+' || c == '-' || c == '.' || c == '_' || c == '?' || c == '='))
+            throw Error("store path '%s' contains illegal character '%s'", path, c);
+}
+
+StorePath::StorePath(std::string_view _baseName)
+    : baseName(_baseName)
+{
+    if (baseName.size() < HashLen + 1)
+        throw Error("'%s' is too short to be a valid store path", baseName);
+    for (auto c : hashPart())
+        if (c == 'e' || c == 'o' || c == 'u' || c == 't'
+            || !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'z')))
+            throw Error("store path '%s' contains illegal base-32 character '%s'", baseName, c);
+    checkName(baseName, name());
+}
+
+StorePath::StorePath(const Hash & hash, std::string_view _name)
+    : baseName((hash.to_string(Base32, false) + "-").append(std::string(_name)))
+{
+    checkName(baseName, name());
+}
+
+std::pair<std::string_view, StringSet> nix::parsePathWithOutputs(std::string_view s)
+{
+    size_t n = s.find("!");
+    return n == s.npos
+        ? std::make_pair(s, std::set<string>())
+        : std::make_pair(((std::string_view) s).substr(0, n),
+            tokenizeString<std::set<string>>((std::string(s)).substr(n + 1), ","));
+}
+
+StorePathWithOutputs nix::parsePathWithOutputs(const Store &store, const std::string & s) {
+    auto [path, outputs] = nix::parsePathWithOutputs(s);
+    return {parseStorePath(store, std::string(path)), std::move(outputs)};
+}
diff --git a/compat-2.4/include/nix-compat.hh b/compat-2.4/include/nix-compat.hh
new file mode 100644
--- /dev/null
+++ b/compat-2.4/include/nix-compat.hh
@@ -0,0 +1,26 @@
+#pragma once
+#include <nix/store-api.hh>
+#include <nix/derivations.hh>
+#include <nix/path-with-outputs.hh>
+
+#define printPath23(store, path) (path)
+#define compatPathSet(store, paths) (paths)
+#define printPathSet23(store, pathSet) (pathSet)
+#define parseStorePath23(store, path) (path)
+#define parseStorePathSet23(store, pathSet) (pathSet)
+#define parseOptionalStorePath23(store, path) (path)
+#define toDerivedPaths24(x) toDerivedPaths(x)
+
+inline nix::StorePath parseStorePath(const nix::Store &store, const nix::Path path) {
+  return store.parseStorePath(path);
+}
+
+inline void compatComputeFSClosure(nix::Store &store, nix::StorePathSet &pathSet, nix::StorePathSet &closurePaths, bool flipDirection = false, bool includeOutputs = false, bool includeDerivers = false) {
+  store.computeFSClosure(pathSet, closurePaths, flipDirection, includeOutputs, includeDerivers);
+}
+
+namespace nix {
+  inline StorePathWithOutputs parsePathWithOutputs(Store &store, const std::string & s) {
+    return parsePathWithOutputs(store, s);
+  }
+}
diff --git a/hercules-ci-cnix-store.cabal b/hercules-ci-cnix-store.cabal
--- a/hercules-ci-cnix-store.cabal
+++ b/hercules-ci-cnix-store.cabal
@@ -1,7 +1,7 @@
 cabal-version: 2.4
 
 name:           hercules-ci-cnix-store
-version:        0.1.1.0
+version:        0.2.0.0
 synopsis:       Haskell bindings for Nix's libstore
 category:       Nix
 homepage:       https://docs.hercules-ci.com
@@ -13,11 +13,16 @@
 build-type:     Simple
 extra-source-files:
     CHANGELOG.md
+    test/data/*.drv
 
 source-repository head
   type: git
   location: https://github.com/hercules-ci/hercules-ci-agent
 
+flag nix-2_4
+  description: Build for Nix >=2.4pre*
+  default: False
+
 -- match the C++ language standard Nix is using
 common cxx-opts
   cxx-options:
@@ -44,13 +49,46 @@
   import: cxx-opts
   exposed-modules:
       Hercules.CNix
-      Hercules.CNix.Util
-      Hercules.CNix.Store.Context
+      Hercules.CNix.Encapsulation
+      Hercules.CNix.Settings
+      Hercules.CNix.Std.Set
+      Hercules.CNix.Std.String
+      Hercules.CNix.Std.String.Context
+      Hercules.CNix.Std.String.Instances
+      Hercules.CNix.Std.Vector
       Hercules.CNix.Store
+      Hercules.CNix.Store.Context
+      Hercules.CNix.Store.Instances
+      Hercules.CNix.Util
   include-dirs:
       include
+  if flag(nix-2_4)
+    include-dirs:
+        compat-2.4/include
+    cpp-options:
+        -DNIX_2_4
+    pkgconfig-depends:
+        nix-store >= 2.4
+      , nix-main >= 2.4
+  else
+    include-dirs:
+        compat-2.3/include
+    pkgconfig-depends:
+        nix-store >= 2.0 && <2.4
+      , nix-main >= 2.0 && <2.4
+    cxx-sources:
+        compat-2.3/derivation-output.cxx
+        compat-2.3/path.cxx
+    install-includes:
+        nix/path-info.hh
+        nix/path.hh
+        nix/path-compat.hh
+        nix/derivation-output.hh
+        nix/content-address.hh
+
   install-includes:
       hercules-ci-cnix/store.hxx
+      nix-compat.hh
 
   hs-source-dirs: src
   build-depends:
@@ -61,10 +99,9 @@
     , conduit
     , containers
     , protolude
+    , template-haskell
     , unliftio-core
-  pkgconfig-depends:
-      nix-store >= 2.0
-    , nix-main >= 2.0
+    , vector
   extra-libraries:
       boost_context
   default-language: Haskell2010
@@ -77,3 +114,31 @@
     -fwarn-missing-signatures
     -fwarn-name-shadowing
     -fwarn-incomplete-patterns
+
+test-suite test
+  type: exitcode-stdio-1.0
+  main-is: TestMain.hs
+  other-modules:
+      Spec
+      Hercules.CNix.Std.SetSpec
+      Hercules.CNix.Store.DerivationSpec
+      Hercules.CNix.Store.TestUtil
+  hs-source-dirs:
+      test
+  default-extensions: DeriveGeneric DeriveTraversable DisambiguateRecordFields FlexibleContexts InstanceSigs LambdaCase MultiParamTypeClasses NoImplicitPrelude OverloadedStrings RankNTypes TupleSections TypeApplications TypeOperators
+  ghc-options: -Werror=incomplete-patterns -Werror=missing-fields -Wall -fwarn-tabs -fwarn-unused-imports -fwarn-missing-signatures -fwarn-name-shadowing -fwarn-incomplete-patterns -threaded -rtsopts -with-rtsopts=-N
+  build-depends:
+    , base
+    , bytestring
+    , containers
+    , exceptions
+    , hercules-ci-cnix-store
+    , hspec
+    , inline-c
+    , inline-c-cpp
+    , protolude
+    , temporary
+    , text
+  build-tool-depends:
+      hspec-discover:hspec-discover
+  default-language: Haskell2010
diff --git a/include/hercules-ci-cnix/store.hxx b/include/hercules-ci-cnix/store.hxx
--- a/include/hercules-ci-cnix/store.hxx
+++ b/include/hercules-ci-cnix/store.hxx
@@ -1,6 +1,8 @@
 
 #pragma once
 
+#include <nix/path-info.hh>
+
 typedef nix::ref<nix::Store> refStore;
 
 typedef nix::Strings::iterator StringsIterator;
diff --git a/src/Hercules/CNix.hs b/src/Hercules/CNix.hs
--- a/src/Hercules/CNix.hs
+++ b/src/Hercules/CNix.hs
@@ -12,10 +12,12 @@
 -- TODO: No more NixStore when EvalState is already there
 -- TODO: Map Nix-specific C++ exceptions to a CNix exception type
 
+import Data.ByteString.Unsafe (unsafePackMallocCString)
 import Hercules.CNix.Store
 import qualified Language.C.Inline.Cpp as C
 import qualified Language.C.Inline.Cpp.Exceptions as C
 import Protolude hiding (evalState, throwIO)
+import System.IO.Unsafe (unsafePerformIO)
 
 C.context context
 
@@ -96,3 +98,11 @@
   [C.block| void {
     $(Strings *ss)->push_back(std::string($bs-ptr:s, $bs-len:s));
   }|]
+
+nixVersion :: ByteString
+nixVersion = unsafePerformIO $ do
+  p <-
+    [C.exp| const char* {
+      strdup(nix::nixVersion.c_str())
+    }|]
+  unsafePackMallocCString p
diff --git a/src/Hercules/CNix/Encapsulation.hs b/src/Hercules/CNix/Encapsulation.hs
new file mode 100644
--- /dev/null
+++ b/src/Hercules/CNix/Encapsulation.hs
@@ -0,0 +1,19 @@
+{-# LANGUAGE FunctionalDependencies #-}
+
+module Hercules.CNix.Encapsulation
+  ( HasEncapsulation (..),
+    nullableMoveToForeignPtrWrapper,
+  )
+where
+
+import Foreign (Ptr, nullPtr)
+import Prelude
+
+class HasEncapsulation a b | b -> a where
+  -- | Takes ownership of the pointer, freeing/finalizing the pointer when
+  -- collectable.
+  moveToForeignPtrWrapper :: Ptr a -> IO b
+
+nullableMoveToForeignPtrWrapper :: HasEncapsulation a b => Ptr a -> IO (Maybe b)
+nullableMoveToForeignPtrWrapper rawPtr | rawPtr == nullPtr = pure Nothing
+nullableMoveToForeignPtrWrapper rawPtr = Just <$> moveToForeignPtrWrapper rawPtr
diff --git a/src/Hercules/CNix/Settings.hs b/src/Hercules/CNix/Settings.hs
new file mode 100644
--- /dev/null
+++ b/src/Hercules/CNix/Settings.hs
@@ -0,0 +1,99 @@
+{-# LANGUAGE BlockArguments #-}
+{-# LANGUAGE CPP #-}
+{-# LANGUAGE QuasiQuotes #-}
+{-# LANGUAGE TemplateHaskell #-}
+
+module Hercules.CNix.Settings where
+
+import Data.ByteString.Unsafe (unsafePackMallocCString)
+import qualified Data.Set as S
+import Hercules.CNix.Encapsulation (moveToForeignPtrWrapper)
+import qualified Hercules.CNix.Std.Set as Std.Set
+import qualified Hercules.CNix.Std.String as Std.String
+import Hercules.CNix.Std.String.Instances ()
+import qualified Hercules.CNix.Std.Vector as Std.Vector
+import Hercules.CNix.Store.Context (context)
+import qualified Language.C.Inline.Cpp as C
+import Protolude hiding (evalState, throwIO)
+
+C.context
+  ( context
+      <> Std.Set.stdSetCtx
+      <> Std.String.stdStringCtx
+      <> Std.Vector.stdVectorCtx
+  )
+
+C.include "<cstring>"
+C.include "<nix/config.h>"
+C.include "<nix/globals.hh>"
+C.include "<set>"
+C.include "<string>"
+
+byteStringSet :: IO (Ptr (Std.Set.CStdSet Std.String.CStdString)) -> IO (Set ByteString)
+byteStringSet x =
+  x
+    >>= moveToForeignPtrWrapper
+    >>= Std.Set.toListFP
+    >>= traverse Std.String.copyToByteString
+    <&> S.fromList
+
+byteStringList :: IO (Ptr (Std.Vector.CStdVector Std.String.CStdString)) -> IO [ByteString]
+byteStringList x =
+  x
+    >>= moveToForeignPtrWrapper
+    >>= Std.Vector.toListFP
+    >>= traverse Std.String.copyToByteString
+
+getExtraPlatforms :: IO (Set ByteString)
+getExtraPlatforms =
+  byteStringSet
+    [C.block| std::set<std::string>*{
+      return new nix::StringSet(nix::settings.extraPlatforms.get());
+    }|]
+
+getSystem :: IO ByteString
+getSystem =
+  unsafePackMallocCString
+    =<< [C.exp| const char *{
+      strdup(nix::settings.thisSystem.get().c_str())
+    }|]
+
+getSystemFeatures :: IO (Set ByteString)
+getSystemFeatures =
+  byteStringSet
+    [C.block| std::set<std::string>*{
+      return new nix::StringSet(nix::settings.systemFeatures.get());
+    }|]
+
+getSubstituters :: IO [ByteString]
+getSubstituters =
+  byteStringList
+    [C.block| std::vector<std::string>*{
+      auto r = new std::vector<std::string>();
+      for (auto i : nix::settings.substituters.get())
+        r->push_back(i);
+      return r;
+    }|]
+
+getTrustedPublicKeys :: IO [ByteString]
+getTrustedPublicKeys =
+  byteStringList
+    [C.block| std::vector<std::string>*{
+      auto r = new std::vector<std::string>();
+      for (auto i : nix::settings.trustedPublicKeys.get())
+        r->push_back(i);
+      return r;
+    }|]
+
+getNarinfoCacheNegativeTtl :: IO Word64
+getNarinfoCacheNegativeTtl =
+  [C.exp| uint64_t{
+    nix::settings.ttlNegativeNarInfoCache.get()
+  }|]
+
+getNetrcFile :: IO ByteString
+getNetrcFile =
+  unsafePackMallocCString
+    =<< [C.exp| const char *{
+      strdup(nix::settings.netrcFile.get().c_str())
+    }|]
diff --git a/src/Hercules/CNix/Std/Set.hs b/src/Hercules/CNix/Std/Set.hs
new file mode 100644
--- /dev/null
+++ b/src/Hercules/CNix/Std/Set.hs
@@ -0,0 +1,192 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE TemplateHaskell #-}
+{-# LANGUAGE TypeApplications #-}
+{-# OPTIONS_GHC -fno-warn-unused-matches #-}
+
+-- | @std::set@
+module Hercules.CNix.Std.Set
+  ( stdSetCtx,
+    instanceStdSet,
+    instanceStdSetCopyable,
+    CStdSet,
+    StdSet (StdSet),
+    Hercules.CNix.Std.Set.new,
+    size,
+    toSet,
+    fromList,
+    fromListP,
+    fromListFP,
+    Hercules.CNix.Std.Set.toList,
+    insert,
+    insertP,
+    insertFP,
+    toListFP,
+  )
+where
+
+import Control.Exception (mask_)
+import Data.Coerce (Coercible, coerce)
+import Data.Foldable (for_)
+import qualified Data.Set as S
+import Data.Traversable (for)
+import qualified Data.Vector.Storable as VS
+import qualified Data.Vector.Storable.Mutable as VSM
+import Foreign
+  ( ForeignPtr,
+    FunPtr,
+    Ptr,
+    Storable,
+    newForeignPtr,
+    withForeignPtr,
+  )
+import Foreign.C (CSize)
+import Hercules.CNix.Encapsulation (HasEncapsulation (..))
+import qualified Language.C.Inline as C
+import qualified Language.C.Inline.Cpp as C
+import qualified Language.C.Inline.Unsafe as CU
+import Language.Haskell.TH (DecsQ)
+import Language.Haskell.TH.Syntax (Dec, Q)
+import Prelude
+
+data CStdSet a
+
+stdSetCtx :: C.Context
+stdSetCtx = C.cppCtx `mappend` C.cppTypePairs [("std::set", [t|CStdSet|])]
+
+newtype StdSet a = StdSet (ForeignPtr (CStdSet a))
+
+instance HasStdSet a => HasEncapsulation (CStdSet a) (StdSet a) where
+  moveToForeignPtrWrapper = fmap StdSet . newForeignPtr cDelete
+
+class HasStdSet a where
+  cNew :: IO (Ptr (CStdSet a))
+  cDelete :: FunPtr (Ptr (CStdSet a) -> IO ())
+  cSize :: Ptr (CStdSet a) -> IO CSize
+  cInsertByPtr :: Ptr a -> Ptr (CStdSet a) -> IO ()
+  cCopies :: Ptr (CStdSet a) -> Ptr (Ptr a) -> IO ()
+
+class HasStdSet a => HasStdSetCopyable a where
+  cCopyTo :: Ptr (CStdSet a) -> Ptr a -> IO ()
+  cInsert :: a -> Ptr (CStdSet a) -> IO ()
+
+-- | Helper for defining templated instances
+roll :: String -> Q [Dec] -> Q [Dec]
+roll cType d =
+  concat
+    <$> sequence
+      [ C.include "<set>",
+        C.include "<algorithm>",
+        C.substitute
+          [ ("T", const cType),
+            ("SET", \var -> "$(std::set<" ++ cType ++ ">* " ++ var ++ ")")
+          ]
+          d
+      ]
+
+instanceStdSet :: String -> DecsQ
+instanceStdSet cType =
+  roll
+    cType
+    [d|
+      instance HasStdSet $(C.getHaskellType False cType) where
+        cNew = [CU.exp| std::set<@T()>* { new std::set<@T()>() } |]
+        cDelete = [C.funPtr| void deleteStdSet(std::set<@T()>* set) { delete set; } |]
+        cSize set = [CU.exp| size_t { @SET(set)->size() } |]
+        cInsertByPtr ptr set = [CU.exp| void { @SET(set)->insert(*$(@T() *ptr)) } |]
+        cCopies set dstPtr =
+          [CU.block| void {
+            const std::set<@T()>& set = *@SET(set);
+            @T()** aim = $(@T()** dstPtr);
+            for (auto item : set) {
+              *aim = new @T()(item);
+              aim++;
+            }
+          }|]
+      |]
+
+instanceStdSetCopyable :: String -> DecsQ
+instanceStdSetCopyable cType =
+  roll
+    cType
+    [d|
+      instance HasStdSetCopyable $(C.getHaskellType False cType) where
+        cCopyTo set dstPtr =
+          [CU.block| void {
+            const std::set<@T()>* set = @SET(set);
+            std::copy(set->begin(), set->end(), $(@T()* dstPtr));
+            } |]
+        cInsert value set =
+          [CU.exp| void { @SET(set)->insert($(@T() value)) }
+        |]
+      |]
+
+new :: forall a. HasStdSet a => IO (StdSet a)
+new = mask_ $ do
+  moveToForeignPtrWrapper =<< cNew @a
+
+size :: HasStdSet a => StdSet a -> IO Int
+size (StdSet fptr) = fromIntegral <$> withForeignPtr fptr cSize
+
+fromList :: HasStdSetCopyable a => [a] -> IO (StdSet a)
+fromList as = do
+  set <- new
+  for_ as $ insert set
+  pure set
+
+fromListP :: HasStdSet a => [Ptr a] -> IO (StdSet a)
+fromListP as = do
+  set <- new
+  for_ as $ insertP set
+  pure set
+
+fromListFP :: (Coercible a' (ForeignPtr a), HasStdSet a) => [a'] -> IO (StdSet a)
+fromListFP as = do
+  set <- new
+  for_ as $ insertFP set
+  pure set
+
+toSet :: (HasStdSetCopyable a, Storable a, Ord a) => StdSet a -> IO (S.Set a)
+toSet stdSet = do
+  S.fromList . VS.toList <$> toVector stdSet
+
+toVector :: (HasStdSetCopyable a, Storable a) => StdSet a -> IO (VS.Vector a)
+toVector stdSet@(StdSet stdSetFPtr) = do
+  vecSize <- size stdSet
+  hsVec <- VSM.new vecSize
+  withForeignPtr stdSetFPtr $ \stdSetPtr ->
+    VSM.unsafeWith hsVec $ \hsVecPtr ->
+      cCopyTo stdSetPtr hsVecPtr
+  VS.unsafeFreeze hsVec
+
+toList :: (HasStdSetCopyable a, Storable a) => StdSet a -> IO [a]
+toList vec = VS.toList <$> toVector vec
+
+toVectorP :: (HasStdSet a) => StdSet a -> IO (VS.Vector (Ptr a))
+toVectorP stdSet@(StdSet stdSetFPtr) = do
+  vecSize <- size stdSet
+  hsVec <- VSM.new vecSize
+  withForeignPtr stdSetFPtr $ \stdSetPtr ->
+    VSM.unsafeWith hsVec $ \hsVecPtr ->
+      cCopies stdSetPtr hsVecPtr
+  VS.unsafeFreeze hsVec
+
+toListP :: (HasStdSet a) => StdSet a -> IO [Ptr a]
+toListP vec = VS.toList <$> toVectorP vec
+
+toListFP :: (HasStdSet a, HasEncapsulation a b) => StdSet a -> IO [b]
+toListFP vec = mask_ $ do
+  ptrs <- toListP vec
+  for ptrs moveToForeignPtrWrapper
+
+insert :: HasStdSetCopyable a => StdSet a -> a -> IO ()
+insert (StdSet fptr) value = withForeignPtr fptr (cInsert value)
+
+insertP :: HasStdSet a => StdSet a -> Ptr a -> IO ()
+insertP (StdSet fptr) ptr = withForeignPtr fptr (cInsertByPtr ptr)
+
+insertFP :: (Coercible a' (ForeignPtr a), HasStdSet a) => StdSet a -> a' -> IO ()
+insertFP (StdSet fptr) vfptr =
+  withForeignPtr fptr $ \setPtr ->
+    withForeignPtr (coerce vfptr) (\valPtr -> cInsertByPtr valPtr setPtr)
diff --git a/src/Hercules/CNix/Std/String.hs b/src/Hercules/CNix/Std/String.hs
new file mode 100644
--- /dev/null
+++ b/src/Hercules/CNix/Std/String.hs
@@ -0,0 +1,93 @@
+{-# LANGUAGE BlockArguments #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE TemplateHaskell #-}
+{-# LANGUAGE TypeApplications #-}
+
+module Hercules.CNix.Std.String
+  ( -- * Context
+    CStdString,
+    stdStringCtx,
+
+    -- * Functions
+    moveToByteString,
+    withString,
+
+    -- * Low level functions
+    new,
+    delete,
+
+    -- * Wrapper-based functions
+    copyToByteString,
+  )
+where
+
+import Control.Exception (bracket, mask_)
+import Data.ByteString (ByteString)
+import Data.ByteString.Unsafe (unsafePackMallocCStringLen)
+import Foreign hiding (new)
+import Hercules.CNix.Encapsulation
+import Hercules.CNix.Std.String.Context
+import Hercules.CNix.Std.String.Instances ()
+import qualified Language.C.Inline as C
+import System.IO.Unsafe (unsafePerformIO)
+import Prelude
+
+C.context (stdStringCtx <> C.bsCtx <> C.fptrCtx)
+
+C.include "<string>"
+C.include "<cstring>"
+
+moveToByteString :: Ptr CStdString -> IO ByteString
+moveToByteString s = mask_ $ alloca \ptr -> alloca \sz -> do
+  [C.block| void {
+    const std::string &s = *$(std::string *s);
+    size_t sz = *$(size_t *sz) = s.size();
+    char *ptr = *$(char **ptr) = (char*)malloc(sz);
+    std::memcpy((void *)ptr, s.c_str(), sz);
+  }|]
+  sz' <- peek sz
+  ptr' <- peek ptr
+  unsafePackMallocCStringLen (ptr', fromIntegral sz')
+
+new :: ByteString -> IO (Ptr CStdString)
+new bs =
+  [C.block| std::string* {
+    return new std::string($bs-ptr:bs, $bs-len:bs);
+  }|]
+
+delete :: Ptr CStdString -> IO ()
+delete bs = [C.block| void { delete $(std::string *bs); }|]
+
+withString :: ByteString -> (Ptr CStdString -> IO a) -> IO a
+withString bs = bracket (new bs) delete
+
+finalize :: FinalizerPtr CStdString
+{-# NOINLINE finalize #-}
+finalize =
+  unsafePerformIO
+    [C.exp|
+      void (*)(std::string *) {
+        [](std::string *v) {
+          delete v;
+        }
+      }
+    |]
+
+newtype StdString = StdString (ForeignPtr CStdString)
+
+instance HasEncapsulation CStdString StdString where
+  moveToForeignPtrWrapper x = StdString <$> newForeignPtr finalize x
+
+copyToByteString :: StdString -> IO ByteString
+copyToByteString (StdString s) = mask_ $ alloca \ptr -> alloca \sz -> do
+  [C.block| void {
+    const std::string &s = *$fptr-ptr:(std::string *s);
+    size_t sz = *$(size_t *sz) = s.size();
+    char *ptr = *$(char **ptr) = (char*)malloc(sz);
+    std::memcpy((void *)ptr, s.c_str(), sz);
+  }|]
+  sz' <- peek sz
+  ptr' <- peek ptr
+  unsafePackMallocCStringLen (ptr', fromIntegral sz')
diff --git a/src/Hercules/CNix/Std/String/Context.hs b/src/Hercules/CNix/Std/String/Context.hs
new file mode 100644
--- /dev/null
+++ b/src/Hercules/CNix/Std/String/Context.hs
@@ -0,0 +1,21 @@
+{-# LANGUAGE QuasiQuotes #-}
+{-# LANGUAGE TemplateHaskell #-}
+
+module Hercules.CNix.Std.String.Context where
+
+import qualified Data.Map as M
+import qualified Language.C.Inline as C
+import qualified Language.C.Inline.Context as C
+import qualified Language.C.Inline.Cpp as C
+import qualified Language.C.Types as C
+import Prelude
+
+data CStdString
+
+stdStringCtx :: C.Context
+stdStringCtx =
+  C.cppCtx
+    <> mempty
+      { C.ctxTypesTable =
+          M.singleton (C.TypeName "std::string") [t|CStdString|]
+      }
diff --git a/src/Hercules/CNix/Std/String/Instances.hs b/src/Hercules/CNix/Std/String/Instances.hs
new file mode 100644
--- /dev/null
+++ b/src/Hercules/CNix/Std/String/Instances.hs
@@ -0,0 +1,19 @@
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE TemplateHaskell #-}
+{-# OPTIONS_GHC -Wno-orphans #-}
+
+-- | Define instances for C++ types in the Context module that can't be in that
+-- module because of TH staging restrictions.
+module Hercules.CNix.Std.String.Instances where
+
+import Data.Semigroup (Semigroup ((<>)))
+import Hercules.CNix.Std.Set
+import Hercules.CNix.Std.String.Context
+import Hercules.CNix.Std.Vector
+import qualified Language.C.Inline.Cpp as C
+
+C.context (stdVectorCtx <> stdSetCtx <> stdStringCtx)
+C.include "<string>"
+
+instanceStdVector "std::string"
+instanceStdSet "std::string"
diff --git a/src/Hercules/CNix/Std/Vector.hs b/src/Hercules/CNix/Std/Vector.hs
new file mode 100644
--- /dev/null
+++ b/src/Hercules/CNix/Std/Vector.hs
@@ -0,0 +1,171 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE TemplateHaskell #-}
+{-# LANGUAGE TypeApplications #-}
+{-# OPTIONS_GHC -fno-warn-unused-matches #-}
+
+-- | @std::vector@
+--
+-- Original author @chpatrick https://github.com/fpco/inline-c/blob/1ba35141e330981fef0457a1619701b8acc32f0b/inline-c-cpp/test/StdVector.hs
+module Hercules.CNix.Std.Vector
+  ( stdVectorCtx,
+    instanceStdVector,
+    instanceStdVectorCopyable,
+    CStdVector,
+    StdVector (StdVector),
+    Hercules.CNix.Std.Vector.new,
+    size,
+    toVector,
+    toVectorP,
+    toListP,
+    toListFP,
+    Hercules.CNix.Std.Vector.toList,
+    Hercules.CNix.Std.Vector.fromList,
+    fromListFP,
+    pushBack,
+    pushBackP,
+    pushBackFP,
+  )
+where
+
+import Control.Exception (mask_)
+import Data.Coerce (Coercible, coerce)
+import Data.Foldable
+import qualified Data.Vector.Storable as VS
+import qualified Data.Vector.Storable.Mutable as VSM
+import Foreign
+import Foreign.C
+import Hercules.CNix.Encapsulation
+import qualified Language.C.Inline as C
+import qualified Language.C.Inline.Cpp as C
+import qualified Language.C.Inline.Unsafe as CU
+import Language.Haskell.TH
+import Prelude
+
+data CStdVector a
+
+stdVectorCtx :: C.Context
+stdVectorCtx = C.cppCtx `mappend` C.cppTypePairs [("std::vector", [t|CStdVector|])]
+
+newtype StdVector a = StdVector (ForeignPtr (CStdVector a))
+
+instance HasStdVector a => HasEncapsulation (CStdVector a) (StdVector a) where
+  moveToForeignPtrWrapper x = StdVector <$> newForeignPtr cDelete x
+
+class HasStdVector a where
+  cNew :: IO (Ptr (CStdVector a))
+  cDelete :: FunPtr (Ptr (CStdVector a) -> IO ())
+  cSize :: Ptr (CStdVector a) -> IO CSize
+  cCopies :: Ptr (CStdVector a) -> Ptr (Ptr a) -> IO ()
+  cPushBackByPtr :: Ptr a -> Ptr (CStdVector a) -> IO ()
+
+class HasStdVector a => HasStdVectorCopyable a where
+  cCopyTo :: Ptr (CStdVector a) -> Ptr a -> IO ()
+  cPushBack :: a -> Ptr (CStdVector a) -> IO ()
+
+-- | Helper for defining templated instances
+roll :: String -> Q [Dec] -> Q [Dec]
+roll cType d =
+  concat
+    <$> sequence
+      [ C.include "<vector>",
+        C.include "<algorithm>",
+        C.substitute
+          [ ("T", const cType),
+            ("VEC", \var -> "$(std::vector<" ++ cType ++ ">* " ++ var ++ ")")
+          ]
+          d
+      ]
+
+instanceStdVector :: String -> DecsQ
+instanceStdVector cType =
+  roll
+    cType
+    [d|
+      instance HasStdVector $(C.getHaskellType False cType) where
+        cNew = [CU.exp| std::vector<@T()>* { new std::vector<@T()>() } |]
+        cDelete = [C.funPtr| void deleteStdVector(std::vector<@T()>* vec) { delete vec; } |]
+        cSize vec = [CU.exp| size_t { @VEC(vec)->size() } |]
+
+        cCopies vec dstPtr =
+          [CU.block| void {
+          const std::vector<@T()>& vec = *@VEC(vec);
+          @T()** aim = $(@T()** dstPtr);
+          for (auto item : vec) {
+            *aim = new @T()(item);
+            aim++;
+          }
+        }|]
+        cPushBackByPtr ptr vec = [CU.exp| void { @VEC(vec)->push_back(*$(@T() *ptr)) } |]
+      |]
+
+instanceStdVectorCopyable :: String -> DecsQ
+instanceStdVectorCopyable cType =
+  roll
+    cType
+    [d|
+      instance HasStdVectorCopyable $(C.getHaskellType False cType) where
+        cCopyTo vec dstPtr =
+          [CU.block| void {
+          const std::vector<@T()>* vec = @VEC(vec);
+          std::copy(vec->begin(), vec->end(), $(@T()* dstPtr));
+          } |]
+        cPushBack value vec = [CU.exp| void { @VEC(vec)->push_back($(@T() value)) } |]
+      |]
+
+new :: forall a. HasStdVector a => IO (StdVector a)
+new = mask_ $ do
+  ptr <- cNew @a
+  StdVector <$> newForeignPtr cDelete ptr
+
+size :: HasStdVector a => StdVector a -> IO Int
+size (StdVector fptr) = fromIntegral <$> withForeignPtr fptr cSize
+
+toVector :: (HasStdVectorCopyable a, Storable a) => StdVector a -> IO (VS.Vector a)
+toVector stdVec@(StdVector stdVecFPtr) = do
+  vecSize <- size stdVec
+  hsVec <- VSM.new vecSize
+  withForeignPtr stdVecFPtr $ \stdVecPtr ->
+    VSM.unsafeWith hsVec $ \hsVecPtr ->
+      cCopyTo stdVecPtr hsVecPtr
+  VS.unsafeFreeze hsVec
+
+toVectorP :: HasStdVector a => StdVector a -> IO (VS.Vector (Ptr a))
+toVectorP stdVec@(StdVector stdVecFPtr) = do
+  vecSize <- size stdVec
+  hsVec <- VSM.new vecSize
+  withForeignPtr stdVecFPtr $ \stdVecPtr ->
+    VSM.unsafeWith hsVec $ \hsVecPtr ->
+      cCopies stdVecPtr hsVecPtr
+  VS.unsafeFreeze hsVec
+
+fromList :: HasStdVectorCopyable a => [a] -> IO (StdVector a)
+fromList as = do
+  vec <- Hercules.CNix.Std.Vector.new
+  for_ as $ \a -> pushBack vec a
+  pure vec
+
+fromListFP :: (Coercible a' (ForeignPtr a), HasStdVector a) => [a'] -> IO (StdVector a)
+fromListFP as = do
+  vec <- Hercules.CNix.Std.Vector.new
+  for_ as $ \a -> pushBackFP vec a
+  pure vec
+
+toList :: (HasStdVectorCopyable a, Storable a) => StdVector a -> IO [a]
+toList vec = VS.toList <$> toVector vec
+
+toListP :: (HasStdVector a) => StdVector a -> IO [Ptr a]
+toListP vec = VS.toList <$> toVectorP vec
+
+toListFP :: (HasEncapsulation a b, HasStdVector a) => StdVector a -> IO [b]
+toListFP vec = traverse moveToForeignPtrWrapper =<< toListP vec
+
+pushBack :: HasStdVectorCopyable a => StdVector a -> a -> IO ()
+pushBack (StdVector fptr) value = withForeignPtr fptr (cPushBack value)
+
+pushBackP :: HasStdVector a => StdVector a -> Ptr a -> IO ()
+pushBackP (StdVector fptr) valueP = withForeignPtr fptr (cPushBackByPtr valueP)
+
+pushBackFP :: (Coercible a' (ForeignPtr a), HasStdVector a) => StdVector a -> a' -> IO ()
+pushBackFP vec vfptr = withForeignPtr (coerce vfptr) (pushBackP vec)
diff --git a/src/Hercules/CNix/Store.hs b/src/Hercules/CNix/Store.hs
--- a/src/Hercules/CNix/Store.hs
+++ b/src/Hercules/CNix/Store.hs
@@ -1,4 +1,6 @@
 {-# LANGUAGE BlockArguments #-}
+{-# LANGUAGE CPP #-}
+{-# LANGUAGE FlexibleInstances #-}
 {-# LANGUAGE QuasiQuotes #-}
 {-# LANGUAGE ScopedTypeVariables #-}
 {-# LANGUAGE TemplateHaskell #-}
@@ -11,17 +13,28 @@
 
 import Control.Exception
 import Control.Monad.IO.Unlift
+import Data.ByteString.Short (ShortByteString)
+import qualified Data.ByteString.Short as SBS
 import Data.ByteString.Unsafe (unsafePackMallocCString)
 import qualified Data.ByteString.Unsafe as BS
 import Data.Coerce (coerce)
 import qualified Data.Map as M
+import Foreign (alloca, free, nullPtr)
 import Foreign.ForeignPtr
+import Foreign.ForeignPtr.Unsafe (unsafeForeignPtrToPtr)
+import Foreign.Storable (peek)
+import Hercules.CNix.Encapsulation (HasEncapsulation (..))
+import Hercules.CNix.Std.Set (StdSet, stdSetCtx)
+import qualified Hercules.CNix.Std.Set as Std.Set
+import Hercules.CNix.Std.String (stdStringCtx)
+import qualified Hercules.CNix.Std.String as Std.String
+import Hercules.CNix.Std.Vector
+import qualified Hercules.CNix.Std.Vector as Std.Vector
 import Hercules.CNix.Store.Context
-  ( Derivation,
-    DerivationInputsIterator,
+  ( DerivationInputsIterator,
     DerivationOutputsIterator,
     NixStore,
-    PathSetIterator,
+    NixStorePath,
     Ref,
     SecretKey,
     StringPairs,
@@ -31,13 +44,14 @@
     unsafeMallocBS,
   )
 import qualified Hercules.CNix.Store.Context as C hiding (context)
+import Hercules.CNix.Store.Instances ()
 import qualified Language.C.Inline.Cpp as C
 import qualified Language.C.Inline.Cpp.Exceptions as C
 import Protolude
 import System.IO.Unsafe (unsafePerformIO)
-import Prelude ()
+import qualified Prelude
 
-C.context context
+C.context (context <> stdVectorCtx <> stdSetCtx <> stdStringCtx)
 
 C.include "<cstring>"
 
@@ -55,18 +69,38 @@
 
 C.include "<nix/globals.hh>"
 
+C.include "<nix/path.hh>"
+
+C.include "<variant>"
+
 C.include "<nix/worker-protocol.hh>"
 
+#ifdef NIX_2_4
+C.include "<nix/path-with-outputs.hh>"
+#endif
+
 C.include "hercules-ci-cnix/store.hxx"
 
+C.include "nix-compat.hh"
+
 C.using "namespace nix"
 
+#ifndef NIX_2_4
+C.using "namespace compat::nix"
+#endif
+
+forNonNull :: Applicative m => Ptr a -> (Ptr a -> m b) -> m (Maybe b)
+forNonNull = flip traverseNonNull
+
+traverseNonNull :: Applicative m => (Ptr a -> m b) -> Ptr a -> m (Maybe b)
+traverseNonNull f p = if p == nullPtr then pure Nothing else Just <$> f p
+
 newtype Store = Store (Ptr (Ref NixStore))
 
 openStore :: IO Store
 openStore =
   coerce
-    [C.throwBlock| refStore* {
+    [C.throwBlock| refStore * {
       refStore s = openStore();
       return new refStore(s);
     } |]
@@ -110,6 +144,15 @@
        return strdup(uri.c_str());
      } |]
 
+-- | Usually @"/nix/store"@
+storeDir :: MonadIO m => Store -> m ByteString
+storeDir (Store store) =
+  unsafeMallocBS
+    [C.block| const char* {
+       std::string uri = (*$(refStore* store))->storeDir;
+       return strdup(uri.c_str());
+     } |]
+
 getStoreProtocolVersion :: Store -> IO Int
 getStoreProtocolVersion (Store store) =
   fromIntegral
@@ -125,10 +168,128 @@
        return PROTOCOL_VERSION;
      } |]
 
-ensurePath :: Store -> ByteString -> IO ()
-ensurePath (Store store) path =
+-- | Store-agnostic store path representation: hash and name. Does not have a storedir or subpath inside the store path.
+newtype StorePath = StorePath (ForeignPtr NixStorePath)
+
+instance HasEncapsulation NixStorePath StorePath where
+  moveToForeignPtrWrapper = moveStorePath
+
+finalizeStorePath :: FinalizerPtr NixStorePath
+{-# NOINLINE finalizeStorePath #-}
+finalizeStorePath =
+  unsafePerformIO
+    [C.exp|
+      void (*)(nix::StorePath *) {
+        [](StorePath *v) {
+          delete v;
+        }
+      }
+    |]
+
+-- | Move ownership of a Ptr NixStorePath into 'StorePath'
+moveStorePath :: Ptr NixStorePath -> IO StorePath
+moveStorePath x = StorePath <$> newForeignPtr finalizeStorePath x
+
+-- | Move ownership of a Ptr NixStorePath into 'StorePath'
+moveStorePathMaybe :: Ptr NixStorePath -> IO (Maybe StorePath)
+moveStorePathMaybe = traverseNonNull $ fmap StorePath . newForeignPtr finalizeStorePath
+
+instance Prelude.Show StorePath where
+  show storePath = unsafePerformIO do
+    bs <-
+      BS.unsafePackMallocCString
+        =<< [C.block| const char* {
+          std::string s($fptr-ptr:(nix::StorePath *storePath)->to_string());
+          return strdup(s.c_str());
+        }|]
+    pure $ toS $ decodeUtf8With lenientDecode bs
+
+instance Eq StorePath where
+  a == b = compare a b == EQ
+
+-- FIXME
+instance Ord StorePath where
+  compare (StorePath a) (StorePath b) =
+    compare
+      0
+      [C.pure| int {
+        $fptr-ptr:(nix::StorePath *a)->to_string().compare($fptr-ptr:(nix::StorePath *b)->to_string())
+      }|]
+
+-- | Create 'StorePath' from hash and name.
+--
+-- Throws C++ `BadStorePath` exception when invalid.
+parseStorePathBaseName :: ByteString -> IO StorePath
+parseStorePathBaseName bs =
+  moveStorePath
+    =<< [C.throwBlock| nix::StorePath *{
+      return new StorePath(std::string($bs-ptr:bs, $bs-len:bs));
+    }|]
+
+-- | Parse a complete store path including storeDir into a 'StorePath'.
+--
+-- Throws C++ `BadStorePath` exception when invalid.
+parseStorePath :: Store -> ByteString -> IO StorePath
+#ifdef NIX_2_4
+parseStorePath (Store store) bs =
+  moveStorePath
+    =<< [C.throwBlock| nix::StorePath *{
+      return new StorePath(std::move((*$(refStore* store))->parseStorePath(std::string($bs-ptr:bs, $bs-len:bs))));
+    }|]
+#else
+parseStorePath (Store store) bs =
+  moveStorePath
+    =<< [C.throwBlock| nix::StorePath *{
+      auto p = canonPath(std::string($bs-ptr:bs, $bs-len:bs));
+      if (dirOf(p) != (*$(refStore* store))->storeDir)
+        throw Error("path '%s' is not in the Nix store", p);
+      return new StorePath(baseNameOf(p));
+    }|]
+#endif
+
+getStorePathBaseName :: StorePath -> IO ByteString
+getStorePathBaseName (StorePath sp) = do
+  BS.unsafePackMallocCString
+    =<< [C.block| const char *{
+      std::string s($fptr-ptr:(nix::StorePath *sp)->to_string());
+      return strdup(s.c_str());
+    }|]
+
+getStorePathHash :: StorePath -> IO ByteString
+getStorePathHash (StorePath sp) = do
+  BS.unsafePackMallocCString
+    =<< [C.block| const char *{
+      std::string s($fptr-ptr:(nix::StorePath *sp)->hashPart());
+      return strdup(s.c_str());
+    }|]
+
+storePathToPath :: Store -> StorePath -> IO ByteString
+#ifdef NIX_2_4
+storePathToPath (Store store) (StorePath sp) =
+  BS.unsafePackMallocCString
+    =<< [C.block| const char *{
+      Store & store = **$(refStore* store);
+      StorePath &sp = *$fptr-ptr:(nix::StorePath *sp);
+      std::string s(store.printStorePath(sp));
+      return strdup(s.c_str());
+    }|]
+#else
+storePathToPath (Store store) (StorePath sp) =
+  BS.unsafePackMallocCString
+    =<< [C.block| const char *{
+      Store & store = **$(refStore* store);
+      StorePath &sp = *$fptr-ptr:(nix::StorePath *sp);
+      std::string s(printPath23(store, sp));
+      return strdup(s.c_str());
+    }|]
+#endif
+
+ensurePath :: Store -> StorePath -> IO ()
+ensurePath (Store store) (StorePath storePath) =
   [C.throwBlock| void {
-    (*$(refStore* store))->ensurePath(std::string($bs-ptr:path, $bs-len:path));
+    Store &store = **$(refStore* store);
+    StorePath &storePath = *$fptr-ptr:(nix::StorePath *storePath);
+    store.ensurePath(printPath23(store, storePath));
   } |]
 
 clearPathInfoCache :: Store -> IO ()
@@ -146,46 +307,67 @@
     }
   } |]
 
-buildPaths :: Store -> [ByteString] -> IO ()
-buildPaths (Store store) paths = do
-  withStringsOf paths $ \pathStrings -> do
-    [C.throwBlock| void {
-      StringSet pathSet;
-      for (auto path : *$(Strings *pathStrings)) {
-        pathSet.insert(path);
+newtype StorePathWithOutputs = StorePathWithOutputs (ForeignPtr C.NixStorePathWithOutputs)
+
+instance HasEncapsulation C.NixStorePathWithOutputs StorePathWithOutputs where
+  moveToForeignPtrWrapper x = StorePathWithOutputs <$> newForeignPtr finalizeStorePathWithOutputs x
+
+finalizeStorePathWithOutputs :: FinalizerPtr C.NixStorePathWithOutputs
+{-# NOINLINE finalizeStorePathWithOutputs #-}
+finalizeStorePathWithOutputs =
+  unsafePerformIO
+    [C.exp|
+      void (*)(nix::StorePathWithOutputs *) {
+        [](StorePathWithOutputs *v) {
+          delete v;
+        }
       }
-      (*$(refStore* store))->buildPaths(pathSet);
+    |]
+
+newStorePathWithOutputs :: StorePath -> [ByteString] -> IO StorePathWithOutputs
+newStorePathWithOutputs storePath outputs = do
+  set <- Std.Set.new
+  for_ outputs (\o -> Std.String.withString o (Std.Set.insertP set))
+  moveToForeignPtrWrapper
+    =<< [C.exp| nix::StorePathWithOutputs * {
+    new StorePathWithOutputs {*$fptr-ptr:(nix::StorePath *storePath), *$fptr-ptr:(std::set<std::string>* set)}
+  }|]
+
+getStorePath :: StorePathWithOutputs -> IO StorePath
+getStorePath swo = mask_ do
+  moveToForeignPtrWrapper
+    =<< [C.exp| nix::StorePath * {
+    new StorePath($fptr-ptr:(nix::StorePathWithOutputs *swo)->path)
+  }|]
+
+getOutputs :: StorePathWithOutputs -> IO [ByteString]
+getOutputs swo = mask_ do
+  traverse Std.String.moveToByteString =<< toListP =<< moveToForeignPtrWrapper
+    =<< [C.throwBlock| std::vector<std::string>* {
+      auto r = new std::vector<std::string>();
+      for (auto s : $fptr-ptr:(nix::StorePathWithOutputs *swo)->outputs)
+        r->push_back(s);
+      return r;
     }|]
 
-buildPath :: Store -> ByteString -> IO ()
-buildPath (Store store) path =
+buildPaths :: Store -> StdVector C.NixStorePathWithOutputs -> IO ()
+buildPaths (Store store) (StdVector paths) = do
   [C.throwBlock| void {
-    PathSet ps({std::string($bs-ptr:path, $bs-len:path)});
-    (*$(refStore* store))->buildPaths(ps);
-   } |]
+    Store &store = **$(refStore* store);
+    std::vector<StorePathWithOutputs> &paths = *$fptr-ptr:(std::vector<nix::StorePathWithOutputs>* paths);
+    store.buildPaths(toDerivedPaths24(printPathSet23(store, paths)));
+  }|]
 
-getDerivation :: Store -> ByteString -> IO (ForeignPtr Derivation)
-getDerivation (Store store) path = do
-  ptr <-
-    [C.throwBlock| Derivation *{
-      return new Derivation(
-          (*$(refStore* store))->derivationFromPath(std::string($bs-ptr:path, $bs-len:path))
-        );
-    } |]
-  newForeignPtr finalizeDerivation ptr
+buildPath :: Store -> StorePathWithOutputs -> IO ()
+buildPath store spwo = do
+  buildPaths store =<< Std.Vector.fromListFP [spwo]
 
--- Useful for testingg
-getDerivationFromFile :: ByteString -> IO (ForeignPtr Derivation)
-getDerivationFromFile path = do
-  ptr <-
-    [C.throwBlock| Derivation *{
-      return new Derivation(
-          readDerivation(std::string($bs-ptr:path, $bs-len:path))
-        );
-    } |]
-  newForeignPtr finalizeDerivation ptr
+newtype Derivation = Derivation (ForeignPtr C.Derivation)
 
-finalizeDerivation :: FinalizerPtr Derivation
+instance HasEncapsulation C.Derivation Derivation where
+  moveToForeignPtrWrapper = fmap Derivation . newForeignPtr finalizeDerivation
+
+finalizeDerivation :: FinalizerPtr C.Derivation
 {-# NOINLINE finalizeDerivation #-}
 finalizeDerivation =
   unsafePerformIO
@@ -196,27 +378,95 @@
       }
     } |]
 
--- | Throws when missing
-getDerivationOutputPath :: ForeignPtr Derivation -> ByteString -> IO ByteString
-getDerivationOutputPath fd outputName = withForeignPtr fd $ \d ->
-  [C.throwBlock|
-    const char *{
-      std::string outputName($bs-ptr:outputName, $bs-len:outputName);
-      Derivation *d = $(Derivation *d);
-      return strdup(d->outputs.at(outputName).path.c_str());
-    }
-  |]
-    >>= BS.unsafePackMallocCString
+getDerivation :: Store -> StorePath -> IO Derivation
+getDerivation (Store store) (StorePath spwo) = do
+  moveToForeignPtrWrapper
+    =<< [C.throwBlock| Derivation *{
+      Store &store = **$(refStore* store);
+      return new Derivation(
+          store.derivationFromPath(printPath23(store, *$fptr-ptr:(nix::StorePath *spwo)))
+        );
+    } |]
 
+-- Useful for testing
+getDerivationFromString ::
+  Store ->
+  -- | Derivation name (store path name with ".drv" extension removed)
+  ByteString ->
+  -- | Contents
+  ByteString ->
+  IO Derivation
+#ifdef NIX_2_4
+getDerivationFromString (Store store) name contents = do
+  moveToForeignPtrWrapper
+    =<< [C.throwBlock| Derivation *{
+      Store &store = **$(refStore* store);
+      std::string name($bs-ptr:name, $bs-len:name);
+      return new Derivation(parseDerivation(store, std::string($bs-ptr:contents, $bs-len:contents), name));
+    }|]
+#else
+getDerivationFromString _store name contents = do
+  moveToForeignPtrWrapper =<< [C.throwBlock| Derivation *{
+      std::string name($bs-ptr:name, $bs-len:name);
+      std::string contents($bs-ptr:contents, $bs-len:contents);
+      AutoDelete tmpDir(createTempDir(), true);
+      Path tmpFile = (Path) tmpDir + "/" + name;
+      writeFile(tmpFile, contents, 0600);
+      return new Derivation(readDerivation(tmpFile));
+    }|]
+#endif
+
+getDerivationNameFromPath :: StorePath -> IO ByteString
+getDerivationNameFromPath storePath =
+  BS.unsafePackMallocCString
+
+#ifdef NIX_2_4
+    =<< [C.throwBlock| const char *{
+      StorePath &sp = *$fptr-ptr:(nix::StorePath *storePath);
+      std::string s(Derivation::nameFromPath(sp));
+      return strdup(s.c_str());
+    }|]
+#else
+    =<< [C.throwBlock| const char *{
+      StorePath &sp = *$fptr-ptr:(nix::StorePath *storePath);
+      std::string pathName(sp.name());
+      assert(isDerivation(pathName));
+      std::string drvName = std::string(pathName, 0, pathName.size() - drvExtension.size());
+      return strdup(drvName.c_str());
+    }|]
+#endif
+
 data DerivationOutput = DerivationOutput
   { derivationOutputName :: !ByteString,
-    derivationOutputPath :: !ByteString,
-    derivationOutputHashAlgo :: !ByteString,
-    derivationOutputHash :: !ByteString
+    derivationOutputPath :: !(Maybe StorePath),
+    derivationOutputDetail :: !DerivationOutputDetail
   }
+  deriving (Eq, Show)
 
-getDerivationOutputs :: ForeignPtr Derivation -> IO [DerivationOutput]
-getDerivationOutputs derivation =
+data DerivationOutputDetail
+  = DerivationOutputInputAddressed StorePath
+  | DerivationOutputCAFixed FixedOutputHash StorePath
+  | DerivationOutputCAFloating FileIngestionMethod HashType
+  | DerivationOutputDeferred
+  deriving (Eq, Show)
+
+data FixedOutputHash = FixedOutputHash !FileIngestionMethod {-# UNPACK #-} !Hash
+  deriving (Eq, Show)
+
+-- | See @content-address.hh@
+data FileIngestionMethod = Flat | Recursive
+  deriving (Eq, Show)
+
+-- | See @hash.hh@
+data Hash = Hash !HashType {-# UNPACK #-} !ShortByteString
+  deriving (Eq, Show)
+
+-- | See @hash.hh@
+data HashType = MD5 | SHA1 | SHA256 | SHA512
+  deriving (Eq, Show)
+
+getDerivationOutputs :: Store -> ByteString -> Derivation -> IO [DerivationOutput]
+getDerivationOutputs (Store store) drvName (Derivation derivation) =
   bracket
     [C.exp| DerivationOutputsIterator* {
       new DerivationOutputsIterator($fptr-ptr:(Derivation *derivation)->outputs.begin())
@@ -226,32 +476,168 @@
       isEnd <- (0 /=) <$> [C.exp| bool { *$(DerivationOutputsIterator *i) == $fptr-ptr:(Derivation *derivation)->outputs.end() }|]
       if isEnd
         then pure []
-        else do
-          name <- [C.exp| const char*{ strdup((*$(DerivationOutputsIterator *i))->first.c_str()) }|] >>= BS.unsafePackMallocCString
-          path <- [C.exp| const char*{ strdup((*$(DerivationOutputsIterator *i))->second.path.c_str()) }|] >>= BS.unsafePackMallocCString
-          hash_ <- [C.exp| const char*{ strdup((*$(DerivationOutputsIterator *i))->second.hash.c_str()) }|] >>= BS.unsafePackMallocCString
-          hashAlgo <- [C.exp| const char*{ strdup((*$(DerivationOutputsIterator *i))->second.hashAlgo.c_str()) }|] >>= BS.unsafePackMallocCString
-          [C.block| void { (*$(DerivationOutputsIterator *i))++; }|]
-          (DerivationOutput name path hashAlgo hash_ :) <$> continue
+        else
+          ( mask_ do
+              alloca \nameP -> alloca \pathP -> alloca \typP -> alloca \fimP ->
+                alloca \hashTypeP -> alloca \hashValueP -> alloca \hashSizeP -> do
+                  [C.throwBlock| void {
+                    Store &store = **$(refStore *store);
+                    std::string drvName = std::string($bs-ptr:drvName, $bs-len:drvName);
+                    nix::DerivationOutputs::iterator &i = *$(DerivationOutputsIterator *i);
+                    const char *&name = *$(const char **nameP);
+                    int &typ = *$(int *typP);
+                    StorePath *& path = *$(nix::StorePath **pathP);
+                    int &fim = *$(int *fimP);
+                    int &hashType = *$(int *hashTypeP);
+                    char *&hashValue = *$(char **hashValueP);
+                    int &hashSize = *$(int *hashSizeP);
 
+                    std::string nameString = i->first;
+                    name = strdup(nameString.c_str());
+                    path = nullptr;
+                    std::visit(overloaded {
+                      [&](DerivationOutputInputAddressed doi) -> void {
+                        typ = 0;
+                        path = new StorePath(doi.path);
+                      },
+                      [&](DerivationOutputCAFixed dof) -> void {
+                        typ = 1;
+#ifdef NIX_2_4
+                        path = new StorePath(dof.path(store, $fptr-ptr:(Derivation *derivation)->name, nameString));
+#else
+                        path = new StorePath(dof.path(store, dof.drvName, nameString));
+#endif
+                        switch (dof.hash.method) {
+                          case nix::FileIngestionMethod::Flat:
+                            fim = 0;
+                            break;
+                          case nix::FileIngestionMethod::Recursive:
+                            fim = 1;
+                            break;
+                          default:
+                            fim = -1;
+                            break;
+                        }
+                        switch (dof.hash.hash.type) {
+                          case htMD5: 
+                            hashType = 0;
+                            break;
+                          case htSHA1: 
+                            hashType = 1;
+                            break;
+                          case htSHA256: 
+                            hashType = 2;
+                            break;
+                          case htSHA512: 
+                            hashType = 3;
+                            break;
+                          default:
+                            hashType = -1;
+                            break;
+                        }
+                        hashSize = dof.hash.hash.hashSize;
+                        hashValue = (char*)malloc(hashSize);
+                        std::memcpy((void*)(hashValue),
+                                    (void*)(dof.hash.hash.hash),
+                                    hashSize);
+                      },
+                      [&](DerivationOutputCAFloating dof) -> void {
+                        typ = 2;
+                        switch (dof.method) {
+                          case nix::FileIngestionMethod::Flat:
+                            fim = 0;
+                            break;
+                          case nix::FileIngestionMethod::Recursive:
+                            fim = 1;
+                            break;
+                          default:
+                            fim = -1;
+                            break;
+                        }
+                        switch (dof.hashType) {
+                          case htMD5: 
+                            hashType = 0;
+                            break;
+                          case htSHA1: 
+                            hashType = 1;
+                            break;
+                          case htSHA256: 
+                            hashType = 2;
+                            break;
+                          case htSHA512: 
+                            hashType = 3;
+                            break;
+                          default:
+                            hashType = -1;
+                            break;
+                        }
+                      },
+                      [&](DerivationOutputDeferred) -> void {
+                        typ = 3;
+                      },
+#ifdef NIX_2_4
+                    }, i->second.output);
+#else
+                    }, compatDerivationOutput(store, drvName, i->second).output);
+#endif
+                    i++;
+                  }|]
+                  name <- unsafePackMallocCString =<< peek nameP
+                  path <- moveStorePathMaybe =<< peek pathP
+                  typ <- peek typP
+                  let getFileIngestionMethod = peek fimP <&> \case 0 -> Flat; 1 -> Recursive; _ -> panic "getDerivationOutputs: unknown fim"
+                      getHashType =
+                        peek hashTypeP <&> \case
+                          0 -> MD5
+                          1 -> SHA1
+                          2 -> SHA256
+                          3 -> SHA512
+                          _ -> panic "getDerivationOutputs: unknown hashType"
+                  detail <- case typ of
+                    0 -> pure $ DerivationOutputInputAddressed (fromMaybe (panic "getDerivationOutputs: impossible DOIA path missing") path)
+                    1 -> do
+                      hashValue <- peek hashValueP
+                      hashSize <- peek hashSizeP
+                      hashString <- SBS.packCStringLen (hashValue, fromIntegral hashSize)
+                      free hashValue
+                      hashType <- getHashType
+                      fim <- getFileIngestionMethod
+                      pure $ DerivationOutputCAFixed (FixedOutputHash fim (Hash hashType hashString)) (fromMaybe (panic "getDerivationOutputs: impossible DOCF path missing") path)
+                    2 -> do
+                      hashType <- getHashType
+                      fim <- getFileIngestionMethod
+                      pure $ DerivationOutputCAFloating fim hashType
+                    3 -> pure DerivationOutputDeferred
+                    _ -> panic "getDerivationOutputs: impossible getDerivationOutputs typ"
+                  pure
+                    ( DerivationOutput
+                        { derivationOutputName = name,
+                          derivationOutputPath = path,
+                          derivationOutputDetail = detail
+                        }
+                        :
+                    )
+          )
+            <*> continue
+
 deleteDerivationOutputsIterator :: Ptr DerivationOutputsIterator -> IO ()
 deleteDerivationOutputsIterator a = [C.block| void { delete $(DerivationOutputsIterator *a); }|]
 
-getDerivationPlatform :: ForeignPtr Derivation -> IO ByteString
+getDerivationPlatform :: Derivation -> IO ByteString
 getDerivationPlatform derivation =
   unsafeMallocBS
     [C.exp| const char* {
        strdup($fptr-ptr:(Derivation *derivation)->platform.c_str())
      } |]
 
-getDerivationBuilder :: ForeignPtr Derivation -> IO ByteString
+getDerivationBuilder :: Derivation -> IO ByteString
 getDerivationBuilder derivation =
   unsafeMallocBS
     [C.exp| const char* {
        strdup($fptr-ptr:(Derivation *derivation)->builder.c_str())
      } |]
 
-getDerivationArguments :: ForeignPtr Derivation -> IO [ByteString]
+getDerivationArguments :: Derivation -> IO [ByteString]
 getDerivationArguments derivation =
   bracket
     [C.throwBlock| Strings* {
@@ -264,21 +650,21 @@
     deleteStrings
     toByteStrings
 
-getDerivationSources :: ForeignPtr Derivation -> IO [ByteString]
-getDerivationSources derivation =
-  bracket
-    [C.throwBlock| Strings* {
-      Strings *r = new Strings();
-      for (auto i : $fptr-ptr:(Derivation *derivation)->inputSrcs) {
-        r->push_back(i);
-      }
-      return r;
-    }|]
-    deleteStrings
-    toByteStrings
+getDerivationSources :: Store -> Derivation -> IO [StorePath]
+getDerivationSources (Store store) derivation = mask_ do
+  vec <-
+    moveToForeignPtrWrapper
+      =<< [C.throwBlock| std::vector<nix::StorePath*>* {
+        Store &store = **$(refStore* store);
+        auto r = new std::vector<StorePath *>();
+        for (auto s : $fptr-ptr:(Derivation *derivation)->inputSrcs)
+          r->push_back(new StorePath(parseStorePath23(store, s)));
+        return r;
+      }|]
+  traverse moveStorePath =<< Std.Vector.toList vec
 
-getDerivationInputs :: ForeignPtr Derivation -> IO [(ByteString, [ByteString])]
-getDerivationInputs derivation =
+getDerivationInputs :: Store -> Derivation -> IO [(StorePath, [ByteString])]
+getDerivationInputs (Store store) derivation =
   bracket
     [C.exp| DerivationInputsIterator* {
       new DerivationInputsIterator($fptr-ptr:(Derivation *derivation)->inputDrvs.begin())
@@ -289,7 +675,12 @@
       if isEnd
         then pure []
         else do
-          name <- [C.exp| const char*{ strdup((*$(DerivationInputsIterator *i))->first.c_str()) }|] >>= BS.unsafePackMallocCString
+          name <-
+            [C.throwBlock| nix::StorePath *{
+              Store &store = **$(refStore* store);
+              return new StorePath(parseStorePath23(store, (*$(DerivationInputsIterator *i))->first));
+            }|]
+              >>= moveStorePath
           outs <-
             bracket
               [C.block| Strings*{ 
@@ -307,7 +698,7 @@
 deleteDerivationInputsIterator :: Ptr DerivationInputsIterator -> IO ()
 deleteDerivationInputsIterator a = [C.block| void { delete $(DerivationInputsIterator *a); }|]
 
-getDerivationEnv :: ForeignPtr Derivation -> IO (Map ByteString ByteString)
+getDerivationEnv :: Derivation -> IO (Map ByteString ByteString)
 getDerivationEnv derivation =
   [C.exp| StringPairs* { &($fptr-ptr:(Derivation *derivation)->env) }|]
     >>= toByteStringMap
@@ -390,16 +781,25 @@
 pushString strings s =
   [C.block| void { $(Strings *strings)->push_back($bs-cstr:s); }|]
 
-copyClosure :: Store -> Store -> [ByteString] -> IO ()
-copyClosure (Store src) (Store dest) paths = do
-  withStringsOf paths $ \pathStrings -> do
+copyClosure :: Store -> Store -> [StorePath] -> IO ()
+copyClosure (Store src) (Store dest) pathList = do
+  (StdVector pathsVector') <- Std.Vector.fromList (pathList <&> \(StorePath c) -> unsafeForeignPtrToPtr c)
+  withForeignPtr pathsVector' \pathsVector ->
     [C.throwBlock| void {
-      StringSet pathSet;
-      for (auto path : *$(Strings *pathStrings)) {
-        pathSet.insert(path);
-      }
-      nix::copyClosure(*$(refStore* src), *$(refStore* dest), pathSet);
+      ref<Store> src = *$(refStore* src);
+      ref<Store> dest = *$(refStore* dest);
+      std::vector<nix::StorePath *> &pathsVector = *$(std::vector<nix::StorePath*>* pathsVector);
+
+      StorePathSet pathSet;
+      for (auto spp : pathsVector)
+        pathSet.insert(*spp);
+
+      StorePathSet closurePaths;
+      compatComputeFSClosure(*src, pathSet, closurePaths);
+
+      nix::copyPaths(src, dest, compatPathSet(*src, closurePaths));
     }|]
+  for_ pathList (\(StorePath c) -> touchForeignPtr c)
 
 parseSecretKey :: ByteString -> IO (ForeignPtr SecretKey)
 parseSecretKey bs =
@@ -424,104 +824,58 @@
   -- | Secret signing key
   Ptr SecretKey ->
   -- | Store path
-  ByteString ->
+  StorePath ->
   -- | False if the signature was already present, True if the signature was added
   IO Bool
-signPath (Store store) secretKey path =
+signPath (Store store) secretKey (StorePath path) =
   (== 1) <$> do
     [C.throwBlock| int {
     nix::ref<nix::Store> store = *$(refStore *store);
-    std::string storePath($bs-cstr:path);
-    auto currentInfo = store->queryPathInfo(storePath);
+    const StorePath &storePath = *$fptr-ptr:(nix::StorePath *path);
+    const SecretKey &secretKey = *$(SecretKey *secretKey);
+    auto currentInfo = store->queryPathInfo(printPath23(*store, storePath));
 
     auto info2(*currentInfo);
     info2.sigs.clear();
-    info2.sign(*$(SecretKey *secretKey));
+#ifdef NIX_2_4
+    info2.sign(*store, secretKey);
+#else
+    info2.sign(secretKey);
+#endif
     assert(!info2.sigs.empty());
     auto sig = *info2.sigs.begin();
 
     if (currentInfo->sigs.count(sig)) {
       return 0;
     } else {
-      store->addSignatures(storePath, info2.sigs);
+      store->addSignatures(printPath23(*store, storePath), info2.sigs);
       return 1;
     }
   }|]
 
------ PathSet -----
-newtype PathSet = PathSet (ForeignPtr (C.StdSet C.StdString))
-
-finalizePathSet :: FinalizerPtr C.PathSet
-{-# NOINLINE finalizePathSet #-}
-finalizePathSet =
-  unsafePerformIO
-    [C.exp|
-  void (*)(PathSet *) {
-    [](PathSet *v){
-      delete v;
-    }
-  } |]
-
-newEmptyPathSet :: IO PathSet
-newEmptyPathSet = do
-  ptr <- [C.exp| PathSet *{ new PathSet() }|]
-  fptr <- newForeignPtr finalizePathSet ptr
-  pure $ PathSet fptr
-
-addToPathSet :: ByteString -> PathSet -> IO ()
-addToPathSet bs pathSet_ = withPathSet pathSet_ $ \pathSet ->
-  [C.throwBlock| void { 
-    $(PathSet *pathSet)->insert(std::string($bs-ptr:bs, $bs-len:bs));
-  }|]
-
-withPathSet :: PathSet -> (Ptr C.PathSet -> IO b) -> IO b
-withPathSet (PathSet pathSetFptr) = withForeignPtr pathSetFptr
-
-traversePathSet :: forall a. (ByteString -> IO a) -> PathSet -> IO [a]
-traversePathSet f pathSet_ = withPathSet pathSet_ $ \pathSet -> do
-  i <- [C.exp| PathSetIterator *{ new PathSetIterator($(PathSet *pathSet)->begin()) }|]
-  end <- [C.exp| PathSetIterator *{ new PathSetIterator ($(PathSet *pathSet)->end()) }|]
-  let cleanup =
-        [C.throwBlock| void {
-          delete $(PathSetIterator *i);
-          delete $(PathSetIterator *end);
-        }|]
-  flip finally cleanup $
-    let go :: ([a] -> [a]) -> IO [a]
-        go acc = do
-          isDone <-
-            [C.exp| int {
-            *$(PathSetIterator *i) == *$(PathSetIterator *end)
-          }|]
-          if isDone /= 0
-            then pure $ acc []
-            else do
-              somePath <- unsafePackMallocCString =<< [C.exp| const char *{ strdup((*$(PathSetIterator *i))->c_str()) } |]
-              a <- f somePath
-              [C.throwBlock| void { (*$(PathSetIterator *i))++; } |]
-              go (acc . (a :))
-     in go identity
-
 -- | Follow symlinks to the store and chop off the parts after the top-level store name
-followLinksToStorePath :: Store -> ByteString -> IO ByteString
+followLinksToStorePath :: Store -> ByteString -> IO StorePath
 followLinksToStorePath (Store store) bs =
-  unsafePackMallocCString
-    =<< [C.throwBlock| const char *{
-    return strdup((*$(refStore* store))->followLinksToStorePath(std::string($bs-ptr:bs, $bs-len:bs)).c_str());
-  }|]
+  moveStorePath
+    =<< [C.throwBlock| nix::StorePath *{
+      Store &store = **$(refStore* store);
+      std::string s = std::string($bs-ptr:bs, $bs-len:bs);
+      return new StorePath(parseStorePath23(store, store.followLinksToStorePath(s)));
+    }|]
 
 queryPathInfo ::
   Store ->
   -- | Exact store path, not a subpath
-  ByteString ->
+  StorePath ->
   -- | ValidPathInfo or exception
   IO (ForeignPtr (Ref ValidPathInfo))
-queryPathInfo (Store store) path = do
+queryPathInfo (Store store) (StorePath path) = do
   vpi <-
-    [C.throwBlock| refValidPathInfo*
-      {
-        return new refValidPathInfo((*$(refStore* store))->queryPathInfo($bs-cstr:path));
-      } |]
+    [C.throwBlock| refValidPathInfo* {
+      Store &store = **$(refStore* store);
+      StorePath &path = *$fptr-ptr:(nix::StorePath *path);
+      return new refValidPathInfo(store.queryPathInfo(printPath23(store, path)));
+    }|]
   newForeignPtr finalizeRefValidPathInfo vpi
 
 finalizeRefValidPathInfo :: FinalizerPtr (Ref ValidPathInfo)
@@ -529,11 +883,11 @@
 finalizeRefValidPathInfo =
   unsafePerformIO
     [C.exp|
-  void (*)(refValidPathInfo *) {
-    [](refValidPathInfo *v){ delete v; }
-  } |]
+      void (*)(refValidPathInfo *) {
+        [](refValidPathInfo *v){ delete v; }
+      }|]
 
--- | The narSize field of a ValidPathInfo struct. Source: store-api.hh
+-- | The narSize field of a ValidPathInfo struct. Source: path-info.hh / store-api.hh
 validPathInfoNarSize :: ForeignPtr (Ref ValidPathInfo) -> Int64
 validPathInfoNarSize vpi =
   fromIntegral $
@@ -542,41 +896,41 @@
         { (*$fptr-ptr:(refValidPathInfo* vpi))->narSize }
       |]
 
--- | Copy the narHash field of a ValidPathInfo struct. Source: store-api.hh
-validPathInfoNarHash :: ForeignPtr (Ref ValidPathInfo) -> IO ByteString
-validPathInfoNarHash vpi =
+-- | Copy the narHash field of a ValidPathInfo struct. Source: path-info.hh / store-api.hh
+validPathInfoNarHash32 :: ForeignPtr (Ref ValidPathInfo) -> IO ByteString
+validPathInfoNarHash32 vpi =
   unsafePackMallocCString
-    =<< [C.exp| const char
-        *{ strdup((*$fptr-ptr:(refValidPathInfo* vpi))->narHash.to_string().c_str()) }
-      |]
+    =<< [C.block| const char *{ 
+      std::string s((*$fptr-ptr:(refValidPathInfo* vpi))->narHash.to_string(nix::Base32, true));
+      return strdup(s.c_str()); }
+    |]
 
 -- | Deriver field of a ValidPathInfo struct. Source: store-api.hh
 --
 -- Returns 'unknownDeriver' when missing.
-validPathInfoDeriver :: ForeignPtr (Ref ValidPathInfo) -> IO ByteString
-validPathInfoDeriver vpi =
-  unsafePackMallocCString
-    =<< [C.throwBlock| const char*
-        {
-          std::optional<Path> deriver = (*$fptr-ptr:(refValidPathInfo* vpi))->deriver;
-          return strdup((deriver == "" ? "unknown-deriver" : deriver->c_str()));
-        }
-      |]
-
--- | String constant representing the case when the deriver of a store path does
--- not exist or is not known. Value: @unknown-deriver@
-unknownDeriver :: Text
-unknownDeriver = "unknown-deriver"
+validPathInfoDeriver :: Store -> ForeignPtr (Ref ValidPathInfo) -> IO (Maybe StorePath)
+validPathInfoDeriver (Store store) vpi =
+  moveStorePathMaybe
+    =<< [C.throwBlock| nix::StorePath * {
+      Store &store = **$(refStore* store);
+      std::optional<StorePath> deriver = parseOptionalStorePath23(store, (*$fptr-ptr:(refValidPathInfo* vpi))->deriver);
+      return deriver ? new StorePath(*deriver) : nullptr;
+    }|]
 
 -- | References field of a ValidPathInfo struct. Source: store-api.hh
-validPathInfoReferences :: ForeignPtr (Ref ValidPathInfo) -> IO PathSet
-validPathInfoReferences vpi = do
-  ptr <-
-    [C.exp| const PathSet*
-            { new PathSet((*$fptr-ptr:(refValidPathInfo* vpi))->references) }
-        |]
-  fptr <- newForeignPtr finalizePathSet ptr
-  pure $ PathSet fptr
+validPathInfoReferences :: Store -> ForeignPtr (Ref ValidPathInfo) -> IO [StorePath]
+validPathInfoReferences (Store store) vpi = do
+  sps <-
+    moveToForeignPtrWrapper
+      =<< [C.throwBlock| std::vector<nix::StorePath *>* {
+        Store &store = **$(refStore* store);
+        auto sps = new std::vector<nix::StorePath *>();
+        for (auto sp : parseStorePathSet23(store, (*$fptr-ptr:(refValidPathInfo* vpi))->references))
+          sps->push_back(new StorePath(sp));
+        return sps;
+      }|]
+  l <- Std.Vector.toList sps
+  for l moveStorePath
 
 ----- computeFSClosure -----
 data ClosureParams = ClosureParams
@@ -593,19 +947,22 @@
       includeDerivers = False
     }
 
-computeFSClosure :: Store -> ClosureParams -> PathSet -> IO PathSet
-computeFSClosure (Store store) params startingSet_ = withPathSet startingSet_ $ \startingSet -> do
+computeFSClosure :: Store -> ClosureParams -> StdSet NixStorePath -> IO (StdSet NixStorePath)
+computeFSClosure (Store store) params (Std.Set.StdSet startingSet) = do
   let countTrue :: Bool -> C.CInt
       countTrue True = 1
       countTrue False = 0
       flipDir = countTrue $ flipDirection params
       inclOut = countTrue $ includeOutputs params
       inclDrv = countTrue $ includeDerivers params
-  ps <-
-    [C.throwBlock| PathSet* {
-             PathSet *r = new PathSet();
-             (*$(refStore* store))->computeFSClosure(*$(PathSet *startingSet), *r, $(int flipDir), $(int inclOut), $(int inclDrv));
-             return r;
-           } |]
-  fp <- newForeignPtr finalizePathSet ps
-  pure $ PathSet fp
+  ret@(Std.Set.StdSet retSet) <- Std.Set.new
+  [C.throwBlock| void {
+    Store &store = **$(refStore* store);
+    StorePathSet &ret = *$fptr-ptr:(std::set<nix::StorePath>* retSet);
+    compatComputeFSClosure(store, *$fptr-ptr:(std::set<nix::StorePath>* startingSet), ret,
+      $(int flipDir), $(int inclOut), $(int inclDrv));
+  }|]
+  pure ret
+
+withPtr' :: (Coercible a' (ForeignPtr a)) => a' -> (Ptr a -> IO b) -> IO b
+withPtr' p = withForeignPtr (coerce p)
diff --git a/src/Hercules/CNix/Store/Context.hs b/src/Hercules/CNix/Store/Context.hs
--- a/src/Hercules/CNix/Store/Context.hs
+++ b/src/Hercules/CNix/Store/Context.hs
@@ -12,23 +12,12 @@
 import qualified Language.C.Types as C
 import Protolude
 
--- | A C++ STL @std::set@, to be used in phantom types.
-data StdSet a
-
--- | A C++ STL @<a>::iterator@
-data Iterator a
-
 -- | A C++ @std::string@
 data StdString
 
 -- | A Nix @ref@, to be used in phantom types.
 data Ref a
 
--- | A Nix @PathSet@ aka @std::set<Path>@ (Nix <= 2.3.*: aka @std::set<std::string>>@)
-type PathSet = StdSet StdString
-
-type PathSetIterator = Iterator StdString
-
 -- | A Nix @Strings@ aka @std::list<std::string>@
 data Strings
 
@@ -50,15 +39,22 @@
 
 data SecretKey
 
+data NixStorePath
+
+data NixStorePathWithOutputs
+
+data CDerivation
+
 context :: C.Context
 context =
   C.cppCtx <> C.fptrCtx
     <> C.bsCtx
+    <> mempty
       { C.ctxTypesTable =
           M.singleton (C.TypeName "refStore") [t|Ref NixStore|]
+            <> M.singleton (C.TypeName "nix::StorePath") [t|NixStorePath|]
+            <> M.singleton (C.TypeName "nix::StorePathWithOutputs") [t|NixStorePathWithOutputs|]
             <> M.singleton (C.TypeName "refValidPathInfo") [t|Ref ValidPathInfo|]
-            <> M.singleton (C.TypeName "PathSet") [t|PathSet|]
-            <> M.singleton (C.TypeName "PathSetIterator") [t|PathSetIterator|]
             <> M.singleton (C.TypeName "Strings") [t|Strings|]
             <> M.singleton (C.TypeName "StringsIterator") [t|StringsIterator|]
             <> M.singleton (C.TypeName "StringPairs") [t|StringPairs|]
diff --git a/src/Hercules/CNix/Store/Instances.hs b/src/Hercules/CNix/Store/Instances.hs
new file mode 100644
--- /dev/null
+++ b/src/Hercules/CNix/Store/Instances.hs
@@ -0,0 +1,34 @@
+{-# LANGUAGE CPP #-}
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE TemplateHaskell #-}
+{-# OPTIONS_GHC -Wno-orphans #-}
+
+-- | Define instances for C++ types in the Context module that can't be in that
+-- module because of TH staging restrictions.
+module Hercules.CNix.Store.Instances where
+
+import Data.Data (Proxy (Proxy))
+import Data.Function (($))
+import Data.Semigroup (Semigroup ((<>)))
+import Hercules.CNix.Std.Set
+import Hercules.CNix.Std.Vector
+import Hercules.CNix.Store.Context
+import qualified Language.C.Inline.Cpp as C
+
+C.context $ context <> stdVectorCtx <> stdSetCtx
+
+C.include "<nix/path.hh>"
+C.include "<nix/derivations.hh>"
+
+#ifdef NIX_2_4
+C.include "<nix/path-with-outputs.hh>"
+#endif
+
+_ = Proxy :: Proxy NixStorePath
+
+instanceStdVector "nix::StorePath *"
+instanceStdVectorCopyable "nix::StorePath *"
+instanceStdSet "nix::StorePath"
+
+instanceStdVector "nix::StorePathWithOutputs *"
+instanceStdVector "nix::StorePathWithOutputs"
diff --git a/test/Hercules/CNix/Std/SetSpec.hs b/test/Hercules/CNix/Std/SetSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/Hercules/CNix/Std/SetSpec.hs
@@ -0,0 +1,28 @@
+{-# LANGUAGE BlockArguments #-}
+{-# LANGUAGE TemplateHaskell #-}
+{-# OPTIONS_GHC -Wno-orphans #-}
+
+module Hercules.CNix.Std.SetSpec where
+
+import Hercules.CNix.Std.Set as Std.Set
+import qualified Language.C.Inline.Cpp as C
+import Protolude
+import Test.Hspec
+
+C.context stdSetCtx
+
+instanceStdSet "int"
+instanceStdSetCopyable "int"
+
+spec :: Spec
+spec =
+  describe "Std.Set" do
+    it "can insert and retrieve copyable values" do
+      set <- Std.Set.new
+      Std.Set.insert set 1
+      Std.Set.insert set 2
+      Std.Set.insert set 3
+      Std.Set.insert set minBound
+      Std.Set.insert set maxBound
+      l <- Std.Set.toList set
+      sort l `shouldBe` ([minBound, 1, 2, 3, maxBound] :: [C.CInt])
diff --git a/test/Hercules/CNix/Store/DerivationSpec.hs b/test/Hercules/CNix/Store/DerivationSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/Hercules/CNix/Store/DerivationSpec.hs
@@ -0,0 +1,53 @@
+{-# LANGUAGE BlockArguments #-}
+
+module Hercules.CNix.Store.DerivationSpec (spec) where
+
+import qualified Data.ByteString as BS
+import qualified Data.Map as M
+import Hercules.CNix.Store (getDerivationArguments, getDerivationBuilder, getDerivationEnv, getDerivationFromString, getDerivationInputs, getDerivationOutputs, getDerivationPlatform, getDerivationSources)
+import Hercules.CNix.Store.TestUtil (withTempStore)
+import Protolude
+import Test.Hspec
+
+spec :: Spec
+spec = do
+  it "parses a fixed-output derivation correctly" $ withTempStore \store -> do
+    bs <- BS.readFile "test/data/fixed-output.drv"
+    let drvName = "hello-2.10.tar.gz"
+    d <- getDerivationFromString store drvName bs
+    platform <- getDerivationPlatform d
+    platform `shouldBe` "x86_64-linux"
+    builder <- getDerivationBuilder d
+    builder `shouldBe` "/nix/store/a3fc4zqaiak11jks9zd579mz5v0li8bg-bash-4.4-p23/bin/bash"
+    arguments <- getDerivationArguments d
+    arguments `shouldBe` ["-e", "/nix/store/720ikgx7yaapyb8hvi8lkicjqwzcx3xr-builder.sh"]
+    sources <- getDerivationSources store d
+    show' sources `shouldBe` "[720ikgx7yaapyb8hvi8lkicjqwzcx3xr-builder.sh]"
+    inputs <- getDerivationInputs store d
+    show' inputs `shouldBe` "[(07apmkf04yjy8scnvz6xh08377kfdyhq-mirrors-list.drv,[\"out\"]),(abwabzabxq9vgd1rcz7m8wwgqd0fdzpw-bash-4.4-p23.drv,[\"out\"]),(fhajhb521vwlcjhm5ymh3pyh7f0rd8ay-curl-7.74.0.drv,[\"dev\"]),(qgdxmika1avgrgxpdz1af8v953c1qgvh-stdenv-linux.drv,[\"out\"])]"
+    env <- getDerivationEnv d
+    env `shouldBe` M.fromList [("SSL_CERT_FILE", "/no-cert-file.crt"), ("buildInputs", ""), ("builder", "/nix/store/a3fc4zqaiak11jks9zd579mz5v0li8bg-bash-4.4-p23/bin/bash"), ("configureFlags", ""), ("curlOpts", ""), ("depsBuildBuild", ""), ("depsBuildBuildPropagated", ""), ("depsBuildTarget", ""), ("depsBuildTargetPropagated", ""), ("depsHostHost", ""), ("depsHostHostPropagated", ""), ("depsTargetTarget", ""), ("depsTargetTargetPropagated", ""), ("doCheck", ""), ("doInstallCheck", ""), ("downloadToTemp", ""), ("executable", ""), ("impureEnvVars", "http_proxy https_proxy ftp_proxy all_proxy no_proxy NIX_CURL_FLAGS NIX_HASHED_MIRRORS NIX_CONNECT_TIMEOUT NIX_MIRRORS_alsa NIX_MIRRORS_apache NIX_MIRRORS_bioc NIX_MIRRORS_bitlbee NIX_MIRRORS_centos NIX_MIRRORS_cpan NIX_MIRRORS_debian NIX_MIRRORS_fedora NIX_MIRRORS_gcc NIX_MIRRORS_gentoo NIX_MIRRORS_gnome NIX_MIRRORS_gnu NIX_MIRRORS_gnupg NIX_MIRRORS_hackage NIX_MIRRORS_hashedMirrors NIX_MIRRORS_imagemagick NIX_MIRRORS_kde NIX_MIRRORS_kernel NIX_MIRRORS_luarocks NIX_MIRRORS_maven NIX_MIRRORS_metalab NIX_MIRRORS_mozilla NIX_MIRRORS_mysql NIX_MIRRORS_oldsuse NIX_MIRRORS_openbsd NIX_MIRRORS_opensuse NIX_MIRRORS_osdn NIX_MIRRORS_postgresql NIX_MIRRORS_pypi NIX_MIRRORS_roy NIX_MIRRORS_sageupstream NIX_MIRRORS_samba NIX_MIRRORS_savannah NIX_MIRRORS_sourceforge NIX_MIRRORS_steamrt NIX_MIRRORS_ubuntu NIX_MIRRORS_xfce NIX_MIRRORS_xorg"), ("mirrorsFile", "/nix/store/mv7ky6l8q2zk8khp7xvmany2nb8phwi6-mirrors-list"), ("name", "hello-2.10.tar.gz"), ("nativeBuildInputs", "/nix/store/0bnxpjfknrwxi8kh9yb2js01xd6wz566-curl-7.74.0-dev"), ("nixpkgsVersion", "20.09"), ("out", "/nix/store/3x7dwzq014bblazs7kq20p9hyzz0qh8g-hello-2.10.tar.gz"), ("outputHash", "0ssi1wpaf7plaswqqjwigppsg5fyh99vdlb9kzl7c9lng89ndq1i"), ("outputHashAlgo", "sha256"), ("outputHashMode", "flat"), ("outputs", "out"), ("patches", ""), ("postFetch", ""), ("preferHashedMirrors", "1"), ("preferLocalBuild", "1"), ("propagatedBuildInputs", ""), ("propagatedNativeBuildInputs", ""), ("showURLs", ""), ("stdenv", "/nix/store/av0pavd6vn698g82ml66gd2hnjd01nzb-stdenv-linux"), ("strictDeps", ""), ("system", "x86_64-linux"), ("urls", "mirror://gnu/hello/hello-2.10.tar.gz")]
+    os <- getDerivationOutputs store drvName d
+    show' os `shouldBe` "[DerivationOutput {derivationOutputName = \"out\", derivationOutputPath = Just 3x7dwzq014bblazs7kq20p9hyzz0qh8g-hello-2.10.tar.gz, derivationOutputDetail = DerivationOutputCAFixed (FixedOutputHash Flat (Hash SHA256 \"1\\224f\\DC3z\\150&v\\232\\159i\\209\\182S\\130\\222\\149\\167\\239}\\145K\\140\\185V\\244\\RS\\167.\\SIQk\")) 3x7dwzq014bblazs7kq20p9hyzz0qh8g-hello-2.10.tar.gz}]"
+
+  it "parses a regular derivation correctly" $ withTempStore \store -> do
+    bs <- BS.readFile "test/data/regular.drv"
+    let drvName = "hello-2.10"
+    d <- getDerivationFromString store drvName bs
+    platform <- getDerivationPlatform d
+    platform `shouldBe` "x86_64-linux"
+    builder <- getDerivationBuilder d
+    builder `shouldBe` "/nix/store/a3fc4zqaiak11jks9zd579mz5v0li8bg-bash-4.4-p23/bin/bash"
+    arguments <- getDerivationArguments d
+    arguments `shouldBe` ["-e", "/nix/store/9krlzvny65gdc8s7kpb6lkx8cd02c25b-default-builder.sh"]
+    sources <- getDerivationSources store d
+    show' sources `shouldBe` "[9krlzvny65gdc8s7kpb6lkx8cd02c25b-default-builder.sh]"
+    inputs <- getDerivationInputs store d
+    show' inputs `shouldBe` "[(7wbzr8nfcibnybvj38rrzkx8xzr7a1xl-stdenv-linux.drv,[\"out\"]),(abwabzabxq9vgd1rcz7m8wwgqd0fdzpw-bash-4.4-p23.drv,[\"out\"]),(w8z5d97dskfaj7wi12132pp7cn69f7xm-hello-2.10.tar.gz.drv,[\"out\"])]"
+    env <- getDerivationEnv d
+    env `shouldBe` M.fromList [("buildInputs", ""), ("builder", "/nix/store/a3fc4zqaiak11jks9zd579mz5v0li8bg-bash-4.4-p23/bin/bash"), ("configureFlags", ""), ("depsBuildBuild", ""), ("depsBuildBuildPropagated", ""), ("depsBuildTarget", ""), ("depsBuildTargetPropagated", ""), ("depsHostHost", ""), ("depsHostHostPropagated", ""), ("depsTargetTarget", ""), ("depsTargetTargetPropagated", ""), ("doCheck", "1"), ("doInstallCheck", ""), ("name", "hello-2.10"), ("nativeBuildInputs", ""), ("out", "/nix/store/8mygch54p7brcqn83xnfa22ik9y3km95-hello-2.10"), ("outputs", "out"), ("patches", ""), ("pname", "hello"), ("propagatedBuildInputs", ""), ("propagatedNativeBuildInputs", ""), ("src", "/nix/store/3x7dwzq014bblazs7kq20p9hyzz0qh8g-hello-2.10.tar.gz"), ("stdenv", "/nix/store/f95jkch26wxmqd076bcm976qrn7ml46i-stdenv-linux"), ("strictDeps", ""), ("system", "x86_64-linux"), ("version", "2.10")]
+    os <- getDerivationOutputs store drvName d
+    show' os `shouldBe` "[DerivationOutput {derivationOutputName = \"out\", derivationOutputPath = Just 8mygch54p7brcqn83xnfa22ik9y3km95-hello-2.10, derivationOutputDetail = DerivationOutputInputAddressed 8mygch54p7brcqn83xnfa22ik9y3km95-hello-2.10}]"
+
+show' :: Show a => a -> Text
+show' = show
diff --git a/test/Hercules/CNix/Store/TestUtil.hs b/test/Hercules/CNix/Store/TestUtil.hs
new file mode 100644
--- /dev/null
+++ b/test/Hercules/CNix/Store/TestUtil.hs
@@ -0,0 +1,12 @@
+{-# LANGUAGE BlockArguments #-}
+
+module Hercules.CNix.Store.TestUtil where
+
+import Hercules.CNix.Store (Store, withStoreFromURI)
+import Protolude
+import System.IO.Temp (withSystemTempDirectory)
+
+withTempStore :: (Store -> IO a) -> IO a
+withTempStore f =
+  withSystemTempDirectory "cnix-test-store" \d ->
+    withStoreFromURI (toS d) f
diff --git a/test/Spec.hs b/test/Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Spec.hs
@@ -0,0 +1,1 @@
+{-# OPTIONS_GHC -F -pgmF hspec-discover -optF --module-name=Spec #-}
diff --git a/test/TestMain.hs b/test/TestMain.hs
new file mode 100644
--- /dev/null
+++ b/test/TestMain.hs
@@ -0,0 +1,18 @@
+{-# LANGUAGE BlockArguments #-}
+
+import Hercules.CNix (init)
+import Protolude
+import qualified Spec
+import System.Mem (performMajorGC)
+import Test.Hspec.Runner
+
+main :: IO ()
+main = do
+  init
+  hspecWith config Spec.spec
+  performMajorGC
+  where
+    config =
+      defaultConfig
+        { configColorMode = ColorAlways
+        }
diff --git a/test/data/fixed-output.drv b/test/data/fixed-output.drv
new file mode 100644
--- /dev/null
+++ b/test/data/fixed-output.drv
@@ -0,0 +1,1 @@
+Derive([("out","/nix/store/3x7dwzq014bblazs7kq20p9hyzz0qh8g-hello-2.10.tar.gz","sha256","31e066137a962676e89f69d1b65382de95a7ef7d914b8cb956f41ea72e0f516b")],[("/nix/store/07apmkf04yjy8scnvz6xh08377kfdyhq-mirrors-list.drv",["out"]),("/nix/store/abwabzabxq9vgd1rcz7m8wwgqd0fdzpw-bash-4.4-p23.drv",["out"]),("/nix/store/fhajhb521vwlcjhm5ymh3pyh7f0rd8ay-curl-7.74.0.drv",["dev"]),("/nix/store/qgdxmika1avgrgxpdz1af8v953c1qgvh-stdenv-linux.drv",["out"])],["/nix/store/720ikgx7yaapyb8hvi8lkicjqwzcx3xr-builder.sh"],"x86_64-linux","/nix/store/a3fc4zqaiak11jks9zd579mz5v0li8bg-bash-4.4-p23/bin/bash",["-e","/nix/store/720ikgx7yaapyb8hvi8lkicjqwzcx3xr-builder.sh"],[("SSL_CERT_FILE","/no-cert-file.crt"),("buildInputs",""),("builder","/nix/store/a3fc4zqaiak11jks9zd579mz5v0li8bg-bash-4.4-p23/bin/bash"),("configureFlags",""),("curlOpts",""),("depsBuildBuild",""),("depsBuildBuildPropagated",""),("depsBuildTarget",""),("depsBuildTargetPropagated",""),("depsHostHost",""),("depsHostHostPropagated",""),("depsTargetTarget",""),("depsTargetTargetPropagated",""),("doCheck",""),("doInstallCheck",""),("downloadToTemp",""),("executable",""),("impureEnvVars","http_proxy https_proxy ftp_proxy all_proxy no_proxy NIX_CURL_FLAGS NIX_HASHED_MIRRORS NIX_CONNECT_TIMEOUT NIX_MIRRORS_alsa NIX_MIRRORS_apache NIX_MIRRORS_bioc NIX_MIRRORS_bitlbee NIX_MIRRORS_centos NIX_MIRRORS_cpan NIX_MIRRORS_debian NIX_MIRRORS_fedora NIX_MIRRORS_gcc NIX_MIRRORS_gentoo NIX_MIRRORS_gnome NIX_MIRRORS_gnu NIX_MIRRORS_gnupg NIX_MIRRORS_hackage NIX_MIRRORS_hashedMirrors NIX_MIRRORS_imagemagick NIX_MIRRORS_kde NIX_MIRRORS_kernel NIX_MIRRORS_luarocks NIX_MIRRORS_maven NIX_MIRRORS_metalab NIX_MIRRORS_mozilla NIX_MIRRORS_mysql NIX_MIRRORS_oldsuse NIX_MIRRORS_openbsd NIX_MIRRORS_opensuse NIX_MIRRORS_osdn NIX_MIRRORS_postgresql NIX_MIRRORS_pypi NIX_MIRRORS_roy NIX_MIRRORS_sageupstream NIX_MIRRORS_samba NIX_MIRRORS_savannah NIX_MIRRORS_sourceforge NIX_MIRRORS_steamrt NIX_MIRRORS_ubuntu NIX_MIRRORS_xfce NIX_MIRRORS_xorg"),("mirrorsFile","/nix/store/mv7ky6l8q2zk8khp7xvmany2nb8phwi6-mirrors-list"),("name","hello-2.10.tar.gz"),("nativeBuildInputs","/nix/store/0bnxpjfknrwxi8kh9yb2js01xd6wz566-curl-7.74.0-dev"),("nixpkgsVersion","20.09"),("out","/nix/store/3x7dwzq014bblazs7kq20p9hyzz0qh8g-hello-2.10.tar.gz"),("outputHash","0ssi1wpaf7plaswqqjwigppsg5fyh99vdlb9kzl7c9lng89ndq1i"),("outputHashAlgo","sha256"),("outputHashMode","flat"),("outputs","out"),("patches",""),("postFetch",""),("preferHashedMirrors","1"),("preferLocalBuild","1"),("propagatedBuildInputs",""),("propagatedNativeBuildInputs",""),("showURLs",""),("stdenv","/nix/store/av0pavd6vn698g82ml66gd2hnjd01nzb-stdenv-linux"),("strictDeps",""),("system","x86_64-linux"),("urls","mirror://gnu/hello/hello-2.10.tar.gz")])
diff --git a/test/data/regular.drv b/test/data/regular.drv
new file mode 100644
--- /dev/null
+++ b/test/data/regular.drv
@@ -0,0 +1,1 @@
+Derive([("out","/nix/store/8mygch54p7brcqn83xnfa22ik9y3km95-hello-2.10","","")],[("/nix/store/7wbzr8nfcibnybvj38rrzkx8xzr7a1xl-stdenv-linux.drv",["out"]),("/nix/store/abwabzabxq9vgd1rcz7m8wwgqd0fdzpw-bash-4.4-p23.drv",["out"]),("/nix/store/w8z5d97dskfaj7wi12132pp7cn69f7xm-hello-2.10.tar.gz.drv",["out"])],["/nix/store/9krlzvny65gdc8s7kpb6lkx8cd02c25b-default-builder.sh"],"x86_64-linux","/nix/store/a3fc4zqaiak11jks9zd579mz5v0li8bg-bash-4.4-p23/bin/bash",["-e","/nix/store/9krlzvny65gdc8s7kpb6lkx8cd02c25b-default-builder.sh"],[("buildInputs",""),("builder","/nix/store/a3fc4zqaiak11jks9zd579mz5v0li8bg-bash-4.4-p23/bin/bash"),("configureFlags",""),("depsBuildBuild",""),("depsBuildBuildPropagated",""),("depsBuildTarget",""),("depsBuildTargetPropagated",""),("depsHostHost",""),("depsHostHostPropagated",""),("depsTargetTarget",""),("depsTargetTargetPropagated",""),("doCheck","1"),("doInstallCheck",""),("name","hello-2.10"),("nativeBuildInputs",""),("out","/nix/store/8mygch54p7brcqn83xnfa22ik9y3km95-hello-2.10"),("outputs","out"),("patches",""),("pname","hello"),("propagatedBuildInputs",""),("propagatedNativeBuildInputs",""),("src","/nix/store/3x7dwzq014bblazs7kq20p9hyzz0qh8g-hello-2.10.tar.gz"),("stdenv","/nix/store/f95jkch26wxmqd076bcm976qrn7ml46i-stdenv-linux"),("strictDeps",""),("system","x86_64-linux"),("version","2.10")])
