From 182580e6457f8571ad22f2270f1e627bbba94328 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emilio=20L=C3=B3pez?= <2642849+elopez@users.noreply.github.com> Date: Thu, 25 Jul 2024 13:54:13 +0200 Subject: [PATCH] Upgrade build to GHC 9.6 (#1245) * Upgrade hevm to latest `echidna-patches` commit * Upgrade build to GHC 9.6 * Refactor terminal and signal support in preparation for Windows UI * Enable UI on all platforms * Use pkgsStatic for Linux pkgsMusl fails to build for some reason. * Remove conditional UI compilation All platforms now support the Echidna UI * Enable UI on ConHost as well See the introduction on https://hackage.haskell.org/package/ansi-terminal-1.1.1/docs/System-Console-ANSI.html * Enable `eager-blackholing` Observed a ~2% speedup on longer runs when compiling both hevm and echidna with this flag. Besides, the Haskell documentation encourages enabling it for parallel code: https://downloads.haskell.org/~ghc/9.6.5/docs/users_guide/using-concurrent.html#compile-time-options-for-smp-parallelism * Resolve hlint warnings * Update Linux Stack CI to 9.6 * Fix compilation failure after merge * Upgrade hevm to `echidna-patches-20240725` * Fix lint warnings, unused imports * flake: align nixpkgs with hevm * flake: fix libiconv for x86_64 darwin > app/utf8-troubleshoot/cbits/locale.c:10:10: error: > fatal error: 'libcharset.h' file not found > | > 10 | #include > | ^ > #include > ^~~~~~~~~~~~~~ Also link iconv dynamically on Darwin as well --- .github/container-linux-static/Dockerfile | 8 +- .../container-linux-static/stack-config.yaml | 5 + .github/workflows/ci.yml | 6 +- flake.lock | 6 +- flake.nix | 70 +++++++---- lib/Echidna/UI.hs | 55 ++++----- lib/Echidna/UI/Widgets.hs | 13 +- package.yaml | 112 +++++++++--------- stack.yaml | 11 +- 9 files changed, 149 insertions(+), 137 deletions(-) create mode 100644 .github/container-linux-static/stack-config.yaml diff --git a/.github/container-linux-static/Dockerfile b/.github/container-linux-static/Dockerfile index 3b37e2bcc..9ab126465 100644 --- a/.github/container-linux-static/Dockerfile +++ b/.github/container-linux-static/Dockerfile @@ -1,4 +1,4 @@ -FROM alpine:3.18.4 +FROM alpine:3.18.6 # Based on https://github.com/fpco/alpine-haskell-stack/blob/9.2.8v2/ghc-Dockerfile RUN apk upgrade --no-cache &&\ @@ -36,12 +36,12 @@ RUN ln -s /usr/lib/libncurses.a /usr/lib/libtinfo.a COPY stack-config.yaml /root/.stack/config.yaml RUN cd /tmp && \ - curl -sSLo /tmp/ghc.tar.xz https://downloads.haskell.org/~ghc/9.4.7/ghc-9.4.7-x86_64-alpine3_12-linux.tar.xz && \ + curl -sSLo /tmp/ghc.tar.xz https://downloads.haskell.org/~ghc/9.6.5/ghc-9.6.5-x86_64-alpine3_12-linux.tar.xz && \ tar xf ghc.tar.xz && \ - cd ghc-9.4.7-x86_64-unknown-linux && \ + cd ghc-9.6.5-x86_64-unknown-linux && \ ./configure --prefix=/usr/local && \ make install && \ - rm -rf /tmp/ghc.tar.xz /tmp/ghc-9.4.7-x86_64-unknown-linux + rm -rf /tmp/ghc.tar.xz /tmp/ghc-9.6.5-x86_64-unknown-linux RUN apk upgrade --no-cache &&\ apk add --no-cache \ diff --git a/.github/container-linux-static/stack-config.yaml b/.github/container-linux-static/stack-config.yaml new file mode 100644 index 000000000..9b5d485a6 --- /dev/null +++ b/.github/container-linux-static/stack-config.yaml @@ -0,0 +1,5 @@ +extra-include-dirs: +- /usr/include +extra-lib-dirs: +- /lib +- /usr/lib diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 182a3dc4e..138b53cd9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,7 +10,7 @@ on: env: # Tag for cache invalidation - CACHE_VERSION: v6 + CACHE_VERSION: v7 jobs: build: @@ -22,7 +22,7 @@ jobs: include: - os: ubuntu-20.04 shell: bash - container: "{\"image\": \"elopeztob/alpine-haskell-stack-echidna:ghc-9.4.7\", \"options\": \"--user 1001\"}" + container: "{\"image\": \"elopeztob/alpine-haskell-stack-echidna:ghc-9.6.5\", \"options\": \"--user 1001\"}" - os: macos-13 # x86_64 macOS shell: bash - os: windows-latest @@ -65,7 +65,7 @@ jobs: id: stack if: matrix.container == '' with: - ghc-version: '9.4' + ghc-version: '9.6' enable-stack: true stack-version: 'latest' diff --git a/flake.lock b/flake.lock index 6f81b13f4..fc545ab1e 100644 --- a/flake.lock +++ b/flake.lock @@ -52,11 +52,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1703499205, - "narHash": "sha256-lF9rK5mSUfIZJgZxC3ge40tp1gmyyOXZ+lRY3P8bfbg=", + "lastModified": 1720368505, + "narHash": "sha256-5r0pInVo5d6Enti0YwUSQK4TebITypB42bWy5su3MrQ=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "e1fa12d4f6c6fe19ccb59cac54b5b3f25e160870", + "rev": "ab82a9612aa45284d4adf69ee81871a389669a9e", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index b774761bb..0e4157443 100644 --- a/flake.nix +++ b/flake.nix @@ -18,7 +18,7 @@ pkgs = nixpkgs.legacyPackages.${system}; # prefer musl on Linux, static glibc + threading does not work properly # TODO: maybe only override it for echidna-redistributable? - pkgsStatic = if pkgs.stdenv.hostPlatform.isLinux then pkgs.pkgsMusl else pkgs; + pkgsStatic = if pkgs.stdenv.hostPlatform.isLinux then pkgs.pkgsStatic else pkgs; # this is not perfect for development as it hardcodes solc to 0.5.7, test suite runs fine though # would be great to integrate solc-select to be more flexible, improve this in future solc = pkgs.stdenv.mkDerivation { @@ -47,39 +47,61 @@ ncurses-static = pkgsStatic.ncurses.override { enableStatic = true; }; - hevm = pkgs: pkgs.haskell.lib.dontCheck ( - pkgs.haskellPackages.callCabal2nix "hevm" (pkgs.fetchFromGitHub { + hsPkgs = ps : + ps.haskellPackages.override { + overrides = hfinal: hprev: { + with-utf8 = + if (with ps.stdenv; hostPlatform.isDarwin && hostPlatform.isx86) + then ps.haskell.lib.compose.overrideCabal (_ : { extraLibraries = [ps.libiconv]; }) hprev.with-utf8 + else hprev.with-utf8; + }; + }; + + cc-workaround-nix-23138 = + pkgs.writeScriptBin "cc-workaround-nix-23138" '' + if [ "$1" = "--print-file-name" ] && [ "$2" = "c++" ]; then + echo c++ + else + exec cc "$@" + fi + ''; + + hevm = pkgs: pkgs.lib.pipe ((hsPkgs pkgs).callCabal2nix "hevm" (pkgs.fetchFromGitHub { owner = "trail-of-forks"; repo = "hevm"; - rev = "7d4344c5e71d14466e86331af064bab61d06bdad"; - sha256 = "sha256-kts6mdwx5KUrVdNztzewWgNM9xGViAhFIZPnWOUllOU="; - }) { secp256k1 = pkgs.secp256k1; }); + rev = "3aba82f06a2d1e0a4a4c26458f747a46dad0e7e2"; + sha256 = "sha256-NXXhEqHTQEL2N9RhXa1eczIsQtIM3mvPfyWXlBXpxK4="; + }) { secp256k1 = pkgs.secp256k1; }) + ([ + pkgs.haskell.lib.compose.dontCheck + ] ++ pkgs.lib.optionals pkgs.stdenv.isDarwin [ + (pkgs.haskell.lib.compose.appendConfigureFlag "--ghc-options=-pgml=${cc-workaround-nix-23138}/bin/cc-workaround-nix-23138") + ]); - # FIXME: figure out solc situation, it conflicts with the one from - # solc-select that is installed with slither, disable tests in the meantime - echidna = pkgs: pkgs.haskell.lib.dontCheck ( - with pkgs; lib.pipe - (haskellPackages.callCabal2nix "echidna" ./. { hevm = hevm pkgs; }) - [ + echidna = pkgs: with pkgs; lib.pipe + ((hsPkgs pkgs).callCabal2nix "echidna" ./. { hevm = hevm pkgs; }) + ([ + # FIXME: figure out solc situation, it conflicts with the one from + # solc-select that is installed with slither, disable tests in the meantime + haskell.lib.compose.dontCheck (haskell.lib.compose.addTestToolDepends [ haskellPackages.hpack slither-analyzer solc ]) (haskell.lib.compose.disableCabalFlag "static") + ] ++ pkgs.lib.optionals pkgs.stdenv.isDarwin [ + (pkgs.haskell.lib.compose.appendConfigureFlag "--ghc-options=-pgml=${cc-workaround-nix-23138}/bin/cc-workaround-nix-23138") ]); echidna-static = with pkgsStatic; lib.pipe (echidna pkgsStatic) [ (haskell.lib.compose.appendConfigureFlags - ([ + [ "--extra-lib-dirs=${stripDylib (gmp.override { withStatic = true; })}/lib" "--extra-lib-dirs=${stripDylib secp256k1-static}/lib" "--extra-lib-dirs=${stripDylib (libff.override { enableStatic = true; })}/lib" - "--extra-lib-dirs=${zlib.static}/lib" + "--extra-lib-dirs=${zlib.override { static = true; shared = false; }}/lib" "--extra-lib-dirs=${stripDylib (libffi.overrideAttrs (_: { dontDisableStatic = true; }))}/lib" "--extra-lib-dirs=${stripDylib (ncurses-static)}/lib" - ] ++ (if stdenv.hostPlatform.isDarwin then [ - "--extra-lib-dirs=${stripDylib (libiconv.override { enableStatic = true; })}/lib" - "--extra-lib-dirs=${stripDylib (libcxxabi)}/lib" - ] else []))) + ]) (haskell.lib.compose.enableCabalFlag "static") ]; @@ -108,10 +130,14 @@ # get the list of dynamic libs from otool and tidy the output libs=$(${otool} -L $out/bin/echidna | tail -n +2 | sed 's/^[[:space:]]*//' | cut -d' ' -f1) # get the path for libcxx - cxx=$(echo "$libs" | ${grep} '^/nix/store/.*-libcxx-') + cxx=$(echo "$libs" | ${grep} '^/nix/store/.*/libc++\.') + cxxabi=$(echo "$libs" | ${grep} '^/nix/store/.*/libc++abi\.') + iconv=$(echo "$libs" | ${grep} '^/nix/store/.*/libiconv\.') # rewrite /nix/... library paths to point to /usr/lib chmod 777 $out/bin/echidna ${install_name_tool} -change "$cxx" /usr/lib/libc++.1.dylib $out/bin/echidna + ${install_name_tool} -change "$cxxabi" /usr/lib/libc++abi.dylib $out/bin/echidna + ${install_name_tool} -change "$iconv" /usr/lib/libiconv.dylib $out/bin/echidna # fix TERMINFO path in ncurses ${perl} -i -pe 's#(${ncurses-static}/share/terminfo)#"/usr/share/terminfo" . "\x0" x (length($1) - 19)#e' $out/bin/echidna # check that no nix deps remain @@ -145,7 +171,11 @@ devShell = with pkgs; haskellPackages.shellFor { packages = _: [ (echidna pkgs) ]; - shellHook = "hpack"; + shellHook = '' + hpack + '' + (if pkgs.stdenv.isDarwin then '' + cabal configure --ghc-options=-pgml=${cc-workaround-nix-23138}/bin/cc-workaround-nix-23138 + '' else ""); buildInputs = [ solc slither-analyzer diff --git a/lib/Echidna/UI.hs b/lib/Echidna/UI.hs index 5387aed51..dcbccdc13 100644 --- a/lib/Echidna/UI.hs +++ b/lib/Echidna/UI.hs @@ -1,19 +1,10 @@ {-# LANGUAGE CPP #-} -{-# OPTIONS_GHC -Wno-unused-imports #-} module Echidna.UI where -#ifdef INTERACTIVE_UI import Brick import Brick.BChan import Brick.Widgets.Dialog qualified as B -import Data.Sequence ((|>)) -import Graphics.Vty (Config, Event(..), Key(..), Modifier(..), defaultConfig, inputMap, mkVty) -import Graphics.Vty qualified as Vty -import System.Posix -import Echidna.UI.Widgets -#endif - import Control.Concurrent (killThread, threadDelay) import Control.Exception (AsyncException) import Control.Monad @@ -24,14 +15,20 @@ import Control.Monad.ST (RealWorld) import Data.ByteString.Lazy qualified as BS import Data.List.Split (chunksOf) import Data.Map (Map) -import Data.Maybe (fromMaybe, isJust) +import Data.Maybe (isJust) +import Data.Sequence ((|>)) import Data.Text (Text) import Data.Time +import Graphics.Vty.Config (VtyUserConfig, defaultConfig, configInputMap) +import Graphics.Vty.CrossPlatform (mkVty) +import Graphics.Vty.Input.Events +import Graphics.Vty qualified as Vty +import System.Console.ANSI (hNowSupportsANSI) +import System.Signal import UnliftIO ( MonadUnliftIO, IORef, newIORef, readIORef, hFlush, stdout , writeIORef, timeout) import UnliftIO.Concurrent hiding (killThread, threadDelay) -import EVM.Solidity (SolcContract) import EVM.Types (Addr, Contract, VM, VMType(Concrete), W256) import Echidna.ABI @@ -43,11 +40,10 @@ import Echidna.Types.Campaign import Echidna.Types.Config import Echidna.Types.Corpus qualified as Corpus import Echidna.Types.Coverage (scoveragePoints) -import Echidna.Types.Solidity (SolConf(..)) import Echidna.Types.Test (EchidnaTest(..), didFail, isOptimizationTest) import Echidna.Types.Tx (Tx) -import Echidna.Types.World (World) import Echidna.UI.Report +import Echidna.UI.Widgets import Echidna.Utility (timePrefix, getTimestamp) data UIEvent = @@ -93,7 +89,6 @@ ui vm dict initialCorpus cliSelectedContract = do uncurry (spawnWorker env perWorkerTestLimit) case effectiveMode of -#ifdef INTERACTIVE_UI Interactive -> do -- Channel to push events to update UI uiChannel <- liftIO $ newBChan 1000 @@ -157,20 +152,17 @@ ui vm dict initialCorpus cliSelectedContract = do liftIO . putStrLn =<< ppCampaign vm states pure states -#else - Interactive -> error "Interactive UI is not available" -#endif NonInteractive outputFormat -> do serverStopVar <- newEmptyMVar -#ifdef INTERACTIVE_UI - -- Handles ctrl-c, TODO: this doesn't work on Windows + + -- Handles ctrl-c liftIO $ forM_ [sigINT, sigTERM] $ \sig -> - let handler = Catch $ do + let handler _ = do stopWorkers workers void $ tryPutMVar serverStopVar () - in installHandler sig handler Nothing -#endif + in installHandler sig handler + let forwardEvent ev = putStrLn =<< runReaderT (ppLogLine vm ev) env uiEventsForwarderStopVar <- spawnListener forwardEvent @@ -245,7 +237,6 @@ ui vm dict initialCorpus cliSelectedContract = do workerStates workers = forM workers $ \(_, stateRef) -> readIORef stateRef -#ifdef INTERACTIVE_UI -- | Order the workers to stop immediately stopWorkers :: MonadIO m => [(ThreadId, IORef WorkerState)] -> m () stopWorkers workers = @@ -253,12 +244,12 @@ stopWorkers workers = workerState <- readIORef workerStateRef liftIO $ mapM_ killThread (threadId : workerState.runningThreads) -vtyConfig :: IO Config +vtyConfig :: IO VtyUserConfig vtyConfig = do - config <- Vty.standardIOConfig - pure config { inputMap = (Nothing, "\ESC[6;2~", EvKey KPageDown [MShift]) : - (Nothing, "\ESC[5;2~", EvKey KPageUp [MShift]) : - inputMap defaultConfig } + pure defaultConfig { configInputMap = [ + (Nothing, "\ESC[6;2~", EvKey KPageDown [MShift]), + (Nothing, "\ESC[5;2~", EvKey KPageUp [MShift]) + ] } -- | Check if we should stop drawing (or updating) the dashboard, then do the right thing. monitor :: MonadReader Env m => m (App UIState UIEvent Name) @@ -336,16 +327,10 @@ monitor = do , appAttrMap = const attrs , appChooseCursor = neverShowCursor } -#endif -- | Heuristic check that we're in a sensible terminal (not a pipe) isTerminal :: IO Bool -isTerminal = -#ifdef INTERACTIVE_UI - (&&) <$> queryTerminal (Fd 0) <*> queryTerminal (Fd 1) -#else - pure False -#endif +isTerminal = hNowSupportsANSI stdout -- | Composes a compact text status line of the campaign statusLine diff --git a/lib/Echidna/UI/Widgets.hs b/lib/Echidna/UI/Widgets.hs index a343eb0b9..cfa851bf3 100644 --- a/lib/Echidna/UI/Widgets.hs +++ b/lib/Echidna/UI/Widgets.hs @@ -2,8 +2,6 @@ module Echidna.UI.Widgets where -#ifdef INTERACTIVE_UI - import Brick hiding (style) import Brick.AttrMap qualified as A import Brick.Widgets.Border @@ -147,14 +145,14 @@ logPane uiState = showLogLine :: (LocalTime, CampaignEvent) -> Widget Name showLogLine (time, event@(WorkerEvent workerId workerType _)) = - (withAttr (attrName "time") $ str $ (timePrefix time) <> "[Worker " <> show workerId <> symSuffix <> "] ") + withAttr (attrName "time") (str $ timePrefix time <> "[Worker " <> show workerId <> symSuffix <> "] ") <+> strBreak (ppCampaignEvent event) where symSuffix = case workerType of SymbolicWorker -> ", symbolic" _ -> "" showLogLine (time, event) = - (withAttr (attrName "time") $ str $ (timePrefix time) <> " ") <+> strBreak (ppCampaignEvent event) + withAttr (attrName "time") (str $ timePrefix time <> " ") <+> strBreak (ppCampaignEvent event) summaryWidget :: Env -> UIState -> Widget Name summaryWidget env uiState = @@ -187,7 +185,7 @@ summaryWidget env uiState = <=> str ("New coverage: " <> timeElapsed uiState uiState.lastNewCov <> " ago") <+> fill ' ' rightSide = - padLeft (Pad 1) $ + padLeft (Pad 1) (rpcInfoWidget uiState.fetchedContracts uiState.fetchedSlots env.chainId) timeElapsed :: UIState -> LocalTime -> String @@ -324,7 +322,7 @@ tracesWidget vm = do let traces = stripAnsiEscapeCodes $ showTraceTree dappInfo vm pure $ if T.null traces then str "" - else str "Traces" <+> str ":" <=> (txtBreak traces) + else str "Traces" <+> str ":" <=> txtBreak traces failWidget :: MonadReader Env m @@ -343,7 +341,6 @@ failWidget b test = do ( failureBadge <+> str (" with " ++ show test.result) , shrinkWidget b test <=> titleWidget <=> s <=> str " " <=> traces ) - where optWidget :: MonadReader Env m @@ -406,5 +403,3 @@ strBreak = strWrapWith $ defaultWrapSettings { breakLongWords = True } txtBreak :: Text -> Widget n txtBreak = txtWrapWith $ defaultWrapSettings { breakLongWords = True } - -#endif diff --git a/package.yaml b/package.yaml index 2e4cc7093..b6732053a 100644 --- a/package.yaml +++ b/package.yaml @@ -9,56 +9,22 @@ ghc-options: - -O2 - -Wall - -fno-warn-orphans + - -feager-blackholing # https://github.com/haskell/cabal/issues/4739 - -optP-Wno-nonportable-include-path - -fspecialize-aggressively - -fexpose-all-unfoldings + - -Wunused-packages dependencies: - - base - aeson - - async - - base16-bytestring - - binary - - bytestring - - code-page + - base - containers - - data-bword - - data-dword - - deepseq - - extra - directory - - exceptions - - filepath - - hashable - hevm - - html-entities - - ListLike - MonadRandom - mtl - - optparse-applicative - - optics - - optics-core - - process - - random - - rosezipper - - semver - - split - text - - transformers - - time - - unliftio - - utf8-string - - vector - - with-utf8 - - word-wrap - - yaml - - http-conduit - - html-conduit - - warp - - wai-extra - - xml-conduit - - strip-ansi-escape language: GHC2021 @@ -74,20 +40,57 @@ default-extensions: library: source-dirs: lib/ - -when: - - condition: "!os(windows)" - cpp-options: -DINTERACTIVE_UI - dependencies: - - brick - - unix - - vty + dependencies: + - ansi-terminal + - async + - base16-bytestring + - binary + - brick + - bytestring + - data-bword + - data-dword + - deepseq + - exceptions + - extra + - filepath + - hashable + - html-conduit + - html-entities + - http-conduit + - ListLike + - optics + - optics-core + - process + - random + - rosezipper + - semver + - signal + - split + - strip-ansi-escape + - time + - unliftio + - utf8-string + - vector + - vty + - vty-crossplatform + - wai-extra + - warp + - word-wrap + - xml-conduit + - yaml executables: echidna: main: Main.hs source-dirs: src/ - dependencies: echidna + dependencies: + - code-page + - echidna + - filepath + - hashable + - optparse-applicative + - time + - with-utf8 ghc-options: - -threaded - '"-with-rtsopts=-A64m -N"' @@ -100,23 +103,25 @@ executables: - -O2 - -optl-pthread - condition: os(darwin) - extra-libraries: c++ ld-options: -Wl,-keep_dwarf_unwind ghc-options: -fcompact-unwind - - condition: os(windows) && impl(ghc >= 9.4) - dependencies: system-cxx-std-lib - - condition: os(windows) && impl(ghc < 9.4) - extra-libraries: stdc++ tests: echidna-testsuite: main: Spec.hs source-dirs: src/test dependencies: + - data-dword - echidna + - exceptions + - optics-core + - process + - semver + - split - tasty - tasty-hunit - tasty-quickcheck + - yaml when: - condition: (os(linux) || os(windows)) && flag(static) ghc-options: @@ -126,13 +131,8 @@ tests: - -O2 - -optl-pthread - condition: os(darwin) - extra-libraries: c++ ld-options: -Wl,-keep_dwarf_unwind ghc-options: -fcompact-unwind - - condition: os(windows) && impl(ghc >= 9.4) - dependencies: system-cxx-std-lib - - condition: os(windows) && impl(ghc < 9.4) - extra-libraries: stdc++ flags: static: diff --git a/stack.yaml b/stack.yaml index d63fc4282..12aa54f57 100644 --- a/stack.yaml +++ b/stack.yaml @@ -1,17 +1,14 @@ -resolver: lts-21.17 +resolver: lts-22.19 packages: - '.' extra-deps: - git: https://github.com/trail-of-forks/hevm.git - commit: 7d4344c5e71d14466e86331af064bab61d06bdad + commit: 3aba82f06a2d1e0a4a4c26458f747a46dad0e7e2 -- restless-git-0.7@sha256:346a5775a586f07ecb291036a8d3016c3484ccdc188b574bcdec0a82c12db293,968 -- s-cargot-0.1.4.0@sha256:61ea1833fbb4c80d93577144870e449d2007d311c34d74252850bb48aa8c31fb,3525 -- semver-range-0.2.8@sha256:44918080c220cf67b6e7c8ad16f01f3cfe1ac69d4f72e528e84d566348bb23c3,1941 -- HSH-2.1.3@sha256:71ded11b224f5066373ce985ec63b10c87129850b33916736dd64fa2bea9ea0a,1705 -- spool-0.1@sha256:77780cbfc2c0be23ff2ea9e474062f3df97fcd9db946ee0b3508280a923b83e2,1461 - smt2-parser-0.1.0.1@sha256:1e1a4565915ed851c13d1e6b8bb5185cf5d454da3b43170825d53e221f753d77,1421 - spawn-0.3@sha256:b91e01d8f2b076841410ae284b32046f91471943dc799c1af77d666c72101f02,1162 +- spool-0.1@sha256:77780cbfc2c0be23ff2ea9e474062f3df97fcd9db946ee0b3508280a923b83e2,1461 - strip-ansi-escape-0.1.0.0@sha256:08f2ed93b16086a837ec46eab7ce8d27cf39d47783caaeb818878ea33c2ff75f,1628 +- vty-windows-0.2.0.3@sha256:0c010b1086a725046a8bb08bb1e6bfdfdb3cfe1c72d6fa77c37306ef9ec774d8,2844