packages feed

nix-diff (empty) → 1.0.0

raw patch · 5 files changed

+647/−0 lines, 5 filesdep +Diffdep +attoparsecdep +basesetup-changed

Dependencies added: Diff, attoparsec, base, containers, mtl, nix-derivation, optparse-generic, system-filepath, text, unix, vector

Files

+ LICENSE view
@@ -0,0 +1,30 @@+Copyright (c) 2017 Gabriel Gonzalez++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++    * Redistributions of source code must retain the above copyright+      notice, this list of conditions and the following disclaimer.++    * Redistributions in binary form must reproduce the above+      copyright notice, this list of conditions and the following+      disclaimer in the documentation and/or other materials provided+      with the distribution.++    * Neither the name of Gabriel Gonzalez nor the names of other+      contributors may be used to endorse or promote products derived+      from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,113 @@+# `nix-diff 1.0.0`++This package provides a `nix-diff` executable which explains why two Nix+derivations differ.  The most common use cases for this are:++* Understanding why you have a cache miss+* Verifying a Nix change had the intended effect at the derivation level+* Improving your understanding of how Nix works++For example, consider the following Nix derivation to build a bare-bones NixOS+system:++```nix+let+  nixos = import <nixpkgs/nixos> {+    system = "x86_64-linux";++    configuration = {+      boot.loader.grub.devices = [ "/dev/sda" ];++      fileSystems."/" = {+        device = "/dev/sda";+      };+    };+  };++in+  nixos.system+```++We can use `nix-instantiate` to compute the derivation for the above file:++```bash+$ nix-instantiate example.nix  # Your exact hash might differ+warning: you did not specify ‘--add-root’; the result might be removed by the garbage collector+/nix/store/6z9nr5pzs4j1v9mld517dmlcz61zy78z-nixos-system-nixos-18.03pre119245.5cfd049a03.drv+```++Now, let's add a service to our system definition:++```nix+let+  nixos = import <nixpkgs/nixos> {+    system = "x86_64-linux";++    configuration = {+      boot.loader.grub.devices = [ "/dev/sda" ];++      fileSystems."/" = {+        device = "/dev/sda";+      };++      services.apache-kafka.enable = true;+    };+  };++in+  nixos.system+```++... and compute the derivation for the updated system:++```bash+$ nix-instantiate example.nix+warning: you did not specify ‘--add-root’; the result might be removed by the garbage collector+/nix/store/k05ibijg0kknvwrgfyb7dxwjrs8qrlbj-nixos-system-nixos-18.03pre119245.5cfd049a03.drv+```++We can use `nix-diff` to compare the two computed derivations to determine what+changed about our system:++```bash+$ nix-diff /nix/store/6z9nr5pzs4j1v9mld517dmlcz61zy78z-nixos-system-nixos-18.03pre119245.5cfd049a03.drv /nix/store/k05ibijg0kknvwrgfyb7dxwjrs8qrlbj-nixos-system-nixos-18.03pre119245.5cfd049a03.drv +```++... which produces the following output:++![](https://i.imgur.com/KUB4rXx.png)++## Development status++[![Build Status](https://travis-ci.org/Gabriel439/nix-diff.png)](https://travis-ci.org/Gabriel439/nix-diff)++I don't currently plan to add any new features, but I do welcome feature+requests.  Just open an issue on the issue tracker if you would like to request+an improvement.++## License (BSD 3-clause)++    Copyright (c) 2017 Gabriel Gonzalez+    All rights reserved.+    +    Redistribution and use in source and binary forms, with or without modification,+    are permitted provided that the following conditions are met:+        * Redistributions of source code must retain the above copyright notice,+          this list of conditions and the following disclaimer.+        * Redistributions in binary form must reproduce the above copyright notice,+          this list of conditions and the following disclaimer in the documentation+          and/or other materials provided with the distribution.+        * Neither the name of Gabriel Gonzalez nor the names of other contributors+          may be used to endorse or promote products derived from this software+          without specific prior written permission.+    +    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND+    ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED+    WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE+    DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR+    ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES+    (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;+    LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON+    ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+    (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS+    SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ nix-diff.cabal view
@@ -0,0 +1,33 @@+name:                nix-diff+version:             1.0.0+synopsis:            Explain why two Nix derivations differ+description:         This package provides a @nix-diff@ executable which+                     explains why two Nix derivations (i.e. @*.drv@ files)+                     differ+homepage:            https://github.com/Gabriel439/nix-diff+license:             BSD3+license-file:        LICENSE+author:              Gabriel Gonzalez+maintainer:          Gabriel439@gmail.com+copyright:           2017 Gabriel Gonzalez+category:            System+build-type:          Simple+tested-with:         GHC == 8.0.2, GHC == 8.2.2+cabal-version:       >= 1.10+extra-source-files:  README.md++executable nix-diff+  main-is:             Main.hs+  build-depends:       base             >= 4.9  && < 4.10+                     , attoparsec       >= 0.13 && < 0.14+                     , containers       >= 0.5  && < 0.6+                     , Diff             >= 0.3  && < 0.4+                     , text             >= 1.2  && < 1.3+                     , system-filepath  >= 0.4  && < 0.5+                     , optparse-generic >= 1.1  && < 1.3+                     , nix-derivation   >= 1.0  && < 1.1+                     , mtl              >= 2.2  && < 2.3+                     , unix                        < 2.8+                     , vector           >= 0.12 && < 0.13+  hs-source-dirs:      src+  default-language:    Haskell2010
+ src/Main.hs view
@@ -0,0 +1,469 @@+{-# LANGUAGE DeriveGeneric              #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE NamedFieldPuns             #-}+{-# LANGUAGE OverloadedStrings          #-}++module Main where++import Control.Monad (forM, forM_)+import Control.Monad.IO.Class (MonadIO, liftIO)+import Control.Monad.Reader (MonadReader, ReaderT, ask, local)+import Control.Monad.State (MonadState, StateT, get, put)+import Data.Attoparsec.Text (IResult(..))+import Data.Map (Map)+import Data.Monoid ((<>))+import Data.Set (Set)+import Data.Text (Text)+import Data.Vector (Vector)+import Filesystem.Path (FilePath)+import Nix.Derivation (Derivation, DerivationOutput)+import Numeric.Natural (Natural)+import Options.Generic (Generic, ParseRecord)+import Prelude hiding (FilePath)++import qualified Control.Monad.Reader+import qualified Control.Monad.State+import qualified Data.Algorithm.Diff+import qualified Data.Attoparsec.Text+import qualified Data.Map+import qualified Data.Set+import qualified Data.Text+import qualified Data.Text.IO+import qualified Data.Vector+import qualified Filesystem.Path.CurrentOS+import qualified Nix.Derivation+import qualified Options.Generic+import qualified System.Posix.IO+import qualified System.Posix.Terminal++data Options = Options FilePath FilePath+    deriving (Generic)++instance ParseRecord Options++data Context = Context+    { tty    :: TTY+    , indent :: Natural+    }++newtype Status = Status { visited :: Set Diffed }++data Diffed = Diffed+    { leftDerivation  :: FilePath+    , leftOutput      :: Set Text+    , rightDerivation :: FilePath+    , rightOutput     :: Set Text+    } deriving (Eq, Ord)++newtype Diff a = Diff { unDiff :: ReaderT Context (StateT Status IO) a }+    deriving+    ( Functor+    , Applicative+    , Monad+    , MonadReader Context+    , MonadState Status+    , MonadIO+    )++echo :: Text -> Diff ()+echo text = do+    Context { indent } <- ask+    let n = fromIntegral indent+    liftIO (Data.Text.IO.putStrLn (Data.Text.replicate n " " <> text))++indented :: Natural -> Diff a -> Diff a+indented n = local adapt+  where+    adapt context = context { indent = indent context + n }++pathToText :: FilePath -> Text+pathToText path =+    case Filesystem.Path.CurrentOS.toText path of+        Left  text -> text+        Right text -> text++{-| Extract the name of a derivation (i.e. the part after the hash)++    This is used to guess which derivations are related to one another, even+    though their hash might differ++    Note that this assumes that the path name is:++    > /nix/store/${32_CHARACTER_HASH}-${NAME}.drv++    Nix technically does not require that the Nix store is actually stored+    underneath `/nix/store`, but this is the overwhelmingly common use case+-}+derivationName :: FilePath -> Text+derivationName = Data.Text.dropEnd 4 . Data.Text.drop 44 . pathToText++-- | Group input derivations by their name+groupByName :: Map FilePath (Set Text) -> Map Text (Map FilePath (Set Text))+groupByName m = Data.Map.fromList assocs+  where+    toAssoc key = (derivationName key, Data.Map.filterWithKey predicate m)+      where+        predicate key' _ = derivationName key == derivationName key'++    assocs = fmap toAssoc (Data.Map.keys m)++-- | Read and parse a derivation from a file+readDerivation :: FilePath -> Diff Derivation+readDerivation path = do+    let string = Filesystem.Path.CurrentOS.encodeString path+    text <- liftIO (Data.Text.IO.readFile string)+    case Data.Attoparsec.Text.parse Nix.Derivation.parseDerivation text of+        Done _ derivation -> do+            return derivation+        _ -> do+            fail ("Could not parse a derivation from this file: " ++ string)++{-| Join two `Map`s on shared keys, discarding keys which are not present in+    both `Map`s+-}+innerJoin :: Ord k => Map k a -> Map k b -> Map k (a, b)+innerJoin = Data.Map.mergeWithKey both left right+  where+    both _ a b = Just (a, b)++    left _ = Data.Map.empty++    right _ = Data.Map.empty++data TTY = IsTTY | NotTTY++-- | Color text red+red :: TTY -> Text -> Text+red  IsTTY text = "\ESC[1;31m" <> text <> "\ESC[0m"+red NotTTY text = text++-- | Color text background red+redBackground  :: TTY -> Text -> Text+redBackground  IsTTY text = "\ESC[41m" <> text <> "\ESC[0m"+redBackground NotTTY text = "←" <> text <> "←"++-- | Color text green+green :: TTY -> Text -> Text+green  IsTTY text = "\ESC[1;32m" <> text <> "\ESC[0m"+green NotTTY text = text++-- | Color text background green+greenBackground :: TTY -> Text -> Text+greenBackground  IsTTY text = "\ESC[42m" <> text <> "\ESC[0m"+greenBackground NotTTY text = "→" <> text <> "→"++-- | Color text grey+grey :: TTY -> Text -> Text+grey  IsTTY text = "\ESC[1;2m" <> text <> "\ESC[0m"+grey NotTTY text = text++-- | Format the left half of a diff+minus :: TTY -> Text -> Text+minus tty text = red tty ("- " <> text)++-- | Format the right half of a diff+plus :: TTY -> Text -> Text+plus tty text = green tty ("+ " <> text)++-- | Format text explaining a diff+explain :: Text -> Text+explain text = "• " <> text++{-| Utility to automate a common pattern of printing the two halves of a diff.+    This passes the correct formatting function to each half+-}+diffWith :: a -> a -> ((Text -> Text, a) -> Diff ()) -> Diff ()+diffWith l r k = do+    Context { tty } <- ask+    k (minus tty, l)+    k (plus  tty, r)++-- | Format the derivation outputs+renderOutputs :: Set Text -> Text+renderOutputs outputs =+    ":{" <> Data.Text.intercalate "," (Data.Set.toList outputs) <> "}"++-- | Diff two outputs+diffOutput+    :: Text+    -- ^ Output name+    -> DerivationOutput+    -- ^ Left derivation outputs+    -> DerivationOutput+    -- ^ Right derivation outputs+    -> Diff ()+diffOutput outputName leftOutput rightOutput = do+    -- We deliberately do not include output paths or hashes in the diff since+    -- we already expect them to differ if the inputs differ.  Instead, we focus+    -- only displaying differing inputs.+    let leftHashAlgo  = Nix.Derivation.hashAlgo leftOutput+    let rightHashAlgo = Nix.Derivation.hashAlgo rightOutput+    if leftHashAlgo == rightHashAlgo+    then return ()+    else do+        echo (explain ("{" <> outputName <> "}:"))+        echo (explain "    Hash algorithm:")+        diffWith leftHashAlgo rightHashAlgo $ \(sign, hashAlgo) -> do+            echo ("        " <> sign hashAlgo)++-- | Diff two sets of outputs+diffOutputs+    :: Map Text DerivationOutput+    -- ^ Left derivation outputs+    -> Map Text DerivationOutput+    -- ^ Right derivation outputs+    -> Diff ()+diffOutputs leftOutputs rightOutputs = do+    let leftExtraOutputs  = Data.Map.difference leftOutputs  rightOutputs+    let rightExtraOutputs = Data.Map.difference rightOutputs leftOutputs++    let bothOutputs = innerJoin leftOutputs rightOutputs++    if Data.Map.null leftExtraOutputs && Data.Map.null rightExtraOutputs+        then return ()+        else do+            echo (explain "The set of outputs do not match:")+            diffWith leftExtraOutputs rightExtraOutputs $ \(sign, extraOutputs) -> do+                forM_ (Data.Map.toList extraOutputs) $ \(key, _value) -> do+                    echo ("    " <> sign ("{" <> key <> "}"))+    forM_ (Data.Map.toList bothOutputs) $ \(key, (leftOutput, rightOutput)) -> do+        if leftOutput == rightOutput+        then return ()+        else diffOutput key leftOutput rightOutput++-- | Diff two `Text` values+diffText+    :: Text+    -- ^ Left value to compare+    -> Text+    -- ^ Right value to compare+    -> Diff Text+diffText left right = do+    Context { indent, tty } <- ask+    let n = fromIntegral indent++    let leftString  = Data.Text.unpack left+        rightString = Data.Text.unpack right++    let chunks = Data.Algorithm.Diff.getGroupedDiff leftString rightString++    let prefix = Data.Text.replicate n " "++    let format text =+            if 80 <= n + Data.Text.length text+            then "''\n" <> indentedText <> prefix <> "''"+            else text+          where+            indentedText =+                (Data.Text.unlines . fmap indentLine . Data.Text.lines) text+              where+                indentLine line = prefix <> "    " <> line++    let renderChunk (Data.Algorithm.Diff.First  l) =+            redBackground   tty (Data.Text.pack l)+        renderChunk (Data.Algorithm.Diff.Second r) =+            greenBackground tty (Data.Text.pack r)+        renderChunk (Data.Algorithm.Diff.Both l _) =+            grey            tty (Data.Text.pack l)++    return (format (Data.Text.concat (fmap renderChunk chunks)))++-- | Diff two environments+diffEnv+    :: Set Text+    -- ^ Left derivation outputs+    -> Set Text+    -- ^ Right derivation outputs+    -> Map Text Text+    -- ^ Left environment to compare+    -> Map Text Text+    -- ^ Right environment to compare+    -> Diff ()+diffEnv leftOutputs rightOutputs leftEnv rightEnv = do+    let leftExtraEnv  = Data.Map.difference leftEnv  rightEnv+    let rightExtraEnv = Data.Map.difference rightEnv leftEnv++    let bothEnv = innerJoin leftEnv rightEnv++    let predicate key (left, right) =+                left == right+            ||  (   Data.Set.member key leftOutputs+                &&  Data.Set.member key rightOutputs+                )+            ||  key == "builder"+            ||  key == "system"++    if     Data.Map.null leftExtraEnv+        && Data.Map.null rightExtraEnv+        && Data.Map.null+               (Data.Map.filterWithKey (\k v -> not (predicate k v)) bothEnv)+    then return ()+    else do+        echo (explain "The environments do not match:")+        diffWith leftExtraEnv rightExtraEnv $ \(sign, extraEnv) -> do+            forM_ (Data.Map.toList extraEnv) $ \(key, value) -> do+                echo ("    " <> sign (key <> "=" <> value))+        forM_ (Data.Map.toList bothEnv) $ \(key, (leftValue, rightValue)) -> do+            if      predicate key (leftValue, rightValue)+            then return ()+            else do+                text <- indented 4 (diffText leftValue rightValue)+                echo ("    " <> key <> "=" <> text)++-- | Diff two environments+diffSrcs+    :: Set FilePath+    -- ^ Left derivation outputs+    -> Set FilePath+    -- ^ Right derivation outputs+    -> Diff Bool+diffSrcs leftSrcs rightSrcs = do+    let leftExtraSrcs  = Data.Set.difference leftSrcs  rightSrcs+    let rightExtraSrcs = Data.Set.difference rightSrcs leftSrcs++    if Data.Set.null leftExtraSrcs && Data.Set.null rightExtraSrcs+        then return True+        else do+            echo (explain "The set of input sources do not match:")+            diffWith leftExtraSrcs rightExtraSrcs $ \(sign, extraSrcs) -> do+                forM_ extraSrcs $ \extraSrc -> do+                    echo ("    " <> sign (pathToText extraSrc))+            return False++diffPlatform :: Text -> Text -> Diff ()+diffPlatform leftPlatform rightPlatform = do+    if leftPlatform == rightPlatform+    then return ()+    else do+        echo (explain "The platforms do not match")+        diffWith leftPlatform rightPlatform $ \(sign, platform) -> do+            echo ("    " <> sign platform)++diffBuilder :: Text -> Text -> Diff ()+diffBuilder leftBuilder rightBuilder = do+    if leftBuilder == rightBuilder+    then return ()+    else do+        echo (explain "The builders do not match")+        diffWith leftBuilder rightBuilder $ \(sign, builder) -> do+            echo ("    " <> sign builder)++diffArgs :: Vector Text -> Vector Text -> Diff ()+diffArgs leftArgs rightArgs = do+    Context { tty } <- ask+    if leftArgs == rightArgs+    then return ()+    else do+        echo (explain "The arguments do not match")+        let leftList  = Data.Vector.toList leftArgs+        let rightList = Data.Vector.toList rightArgs+        let diffs = Data.Algorithm.Diff.getDiff leftList rightList+        let renderDiff (Data.Algorithm.Diff.First arg) =+                echo ("    " <> minus tty arg)+            renderDiff (Data.Algorithm.Diff.Second arg) =+                echo ("    " <> plus tty arg)+            renderDiff (Data.Algorithm.Diff.Both arg _) =+                echo ("    " <> explain arg)+        mapM_ renderDiff diffs++diff :: FilePath -> Set Text -> FilePath -> Set Text -> Diff ()+diff leftPath leftOutputs rightPath rightOutputs = do+    Status { visited } <- get+    let diffed = Diffed leftPath leftOutputs rightPath rightOutputs+    if leftPath == rightPath+    then return ()+    else if Data.Set.member diffed visited+    then do+        echo (explain "These two derivations have already been compared")+    else do+        put (Status (Data.Set.insert diffed visited))+        diffWith (leftPath, leftOutputs) (rightPath, rightOutputs) $ \(sign, (path, outputs)) -> do+            echo (sign (pathToText path <> renderOutputs outputs))++        if derivationName leftPath /= derivationName rightPath+        then do+            echo (explain "The derivation names do not match")+        else if leftOutputs /= rightOutputs+        then do+            echo (explain "The requested outputs do not match")+        else do+            leftDerivation  <- readDerivation leftPath+            rightDerivation <- readDerivation rightPath++            let leftOuts = Nix.Derivation.outputs leftDerivation+            let rightOuts = Nix.Derivation.outputs rightDerivation+            diffOutputs leftOuts rightOuts++            let leftPlatform  = Nix.Derivation.platform leftDerivation+            let rightPlatform = Nix.Derivation.platform rightDerivation+            diffPlatform leftPlatform rightPlatform++            let leftBuilder  = Nix.Derivation.builder leftDerivation+            let rightBuilder = Nix.Derivation.builder rightDerivation+            diffBuilder leftBuilder rightBuilder++            let leftArgs  = Nix.Derivation.args leftDerivation+            let rightArgs = Nix.Derivation.args rightDerivation+            diffArgs leftArgs rightArgs++            let leftInputs  = groupByName (Nix.Derivation.inputDrvs leftDerivation)+            let rightInputs = groupByName (Nix.Derivation.inputDrvs rightDerivation)+    +            let leftNames  = Data.Map.keysSet leftInputs+            let rightNames = Data.Map.keysSet rightInputs+            let leftExtraNames  = Data.Set.difference leftNames  rightNames+            let rightExtraNames = Data.Set.difference rightNames leftNames++            if leftNames /= rightNames+            then do+                echo (explain "The set of input names do not match:")+                diffWith leftExtraNames rightExtraNames $ \(sign, names) -> do+                    forM_ names $ \name -> do+                        echo ("    " <> sign name)+            else do +                let assocs = Data.Map.toList (innerJoin leftInputs rightInputs)+                descended <- forM assocs $ \(inputName, (leftPaths, rightPaths)) -> do+                    let leftExtraPaths =+                            Data.Map.difference leftPaths  rightPaths+                    let rightExtraPaths =+                            Data.Map.difference rightPaths leftPaths+                    case (Data.Map.toList leftExtraPaths, Data.Map.toList rightExtraPaths) of+                        _   | leftPaths == rightPaths -> do+                            return False+                        ([(leftPath', leftOutputs')], [(rightPath', rightOutputs')])+                            | leftOutputs' == rightOutputs' -> do+                            echo (explain ("The input named `" <> inputName <> "` differs"))+                            indented 2 (diff leftPath' leftOutputs' rightPath' rightOutputs')+                            return True+                        _ -> do+                            echo (explain ("The set of inputs named `" <> inputName <> "` do not match"))+                            diffWith leftExtraPaths rightExtraPaths $ \(sign, extraPaths) -> do+                                forM_ (Data.Map.toList extraPaths) $ \(extraPath, outputs) -> do+                                    echo ("    " <> sign (pathToText extraPath <> renderOutputs outputs))+                            return False+                if or descended+                then return ()+                else do+                    let leftSrcs  = Nix.Derivation.inputSrcs leftDerivation+                    let rightSrcs = Nix.Derivation.inputSrcs rightDerivation+                    differed <- diffSrcs leftSrcs rightSrcs++                    if not differed+                    then return ()+                    else do+                        let leftEnv  = Nix.Derivation.env leftDerivation+                        let rightEnv = Nix.Derivation.env rightDerivation+                        let leftOutNames  = Data.Map.keysSet leftOuts+                        let rightOutNames = Data.Map.keysSet rightOuts+                        diffEnv leftOutNames rightOutNames leftEnv rightEnv++main :: IO ()+main = do+    Options left right <- Options.Generic.getRecord "Explain why two derivations differ"+    b <- System.Posix.Terminal.queryTerminal System.Posix.IO.stdOutput+    let tty = if b then IsTTY else NotTTY+    let indent = 0+    let context = Context { tty, indent }+    let status = Status Data.Set.empty+    let action = diff left (Data.Set.singleton "out") right (Data.Set.singleton "out")+    Control.Monad.State.evalStateT (Control.Monad.Reader.runReaderT (unDiff action) context) status