diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,13 @@
 # Revision history for ghcup
 
+## 0.1.17.4 -- 2021-11-13
+
+* add `--metadata-caching` option, allowing to also disable yaml metadata caching wrt [#278](https://gitlab.haskell.org/haskell/ghcup-hs/-/issues/278)
+* make upgrading ghcup in TUI more pleasant wrt [#276](https://gitlab.haskell.org/haskell/ghcup-hs/-/issues/276)
+* fix parsing of atypical GHC versions (e.g. `8.10.5-patch1`)
+* fix compiling HLS dynamically linked, also see [#245](https://gitlab.haskell.org/haskell/ghcup-hs/-/issues/245)
+* redo (and break) some of the `ghcup compile <tool>` interface, improving patch options and setting custom cabal.project files
+
 ## 0.1.17.3 -- 2021-10-27
 
 * clean up during unpack failures as well
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -9,3 +9,5 @@
 GHCup is an installer for the general purpose language [Haskell](https://www.haskell.org/).
 
 Visit the [documentation](https://www.haskell.org/ghcup/) for installation instructions.
+
+If you're looking for the metadata YAML files, see here: [https://github.com/haskell/ghcup-metadata](https://github.com/haskell/ghcup-metadata)
diff --git a/app/ghcup-gen/Main.hs b/app/ghcup-gen/Main.hs
deleted file mode 100644
--- a/app/ghcup-gen/Main.hs
+++ /dev/null
@@ -1,155 +0,0 @@
-{-# LANGUAGE CPP               #-}
-{-# LANGUAGE DataKinds         #-}
-{-# LANGUAGE DuplicateRecordFields #-}
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE TypeApplications  #-}
-
-
-module Main where
-
-import           GHCup.Types
-import           GHCup.Errors
-import           GHCup.Platform
-import           GHCup.Utils.Dirs
-import           GHCup.Utils.Logger
-import           GHCup.Types.JSON               ( )
-
-import           Control.Exception              ( displayException )
-import           Control.Monad.Trans.Reader     ( runReaderT )
-import           Control.Monad.IO.Class
-import           Data.Char                      ( toLower )
-import           Data.Maybe
-#if !MIN_VERSION_base(4,13,0)
-import           Data.Semigroup                 ( (<>) )
-#endif
-import           Options.Applicative     hiding ( style )
-import           Haskus.Utils.Variant.Excepts
-import           System.Console.Pretty
-import           System.Environment
-import           System.Exit
-import           System.IO                      ( stderr )
-import           Text.Regex.Posix
-import           Validate
-import           Text.PrettyPrint.HughesPJClass ( prettyShow )
-
-import qualified Data.Text.IO                  as T
-import qualified Data.Text                     as T
-import qualified Data.ByteString               as B
-import qualified Data.Yaml.Aeson               as Y
-
-
-data Options = Options
-  { optCommand :: Command
-  }
-
-data Command = ValidateYAML ValidateYAMLOpts
-             | ValidateTarballs ValidateYAMLOpts TarballFilter
-
-
-data Input
-  = FileInput FilePath -- optsparse-applicative doesn't handle ByteString correctly anyway
-  | StdInput
-
-fileInput :: Parser Input
-fileInput =
-  FileInput
-    <$> strOption
-          (long "file" <> short 'f' <> metavar "FILENAME" <> help
-            "Input file to validate"
-          )
-
-stdInput :: Parser Input
-stdInput = flag'
-  StdInput
-  (short 'i' <> long "stdin" <> help "Validate from stdin (default)")
-
-inputP :: Parser Input
-inputP = fileInput <|> stdInput
-
-data ValidateYAMLOpts = ValidateYAMLOpts
-  { vInput :: Maybe Input
-  }
-
-validateYAMLOpts :: Parser ValidateYAMLOpts
-validateYAMLOpts = ValidateYAMLOpts <$> optional inputP
-
-tarballFilterP :: Parser TarballFilter
-tarballFilterP = option readm $
-  long "tarball-filter" <> short 'u' <> metavar "<tool>-<version>" <> value def
-    <> help "Only check certain tarballs (format: <tool>-<version>)"
-  where
-    def = TarballFilter (Right Nothing) (makeRegex ("" :: String))
-    readm = do
-      s <- str
-      case span (/= '-') s of
-        (_, []) -> fail "invalid format, missing '-' after the tool name"
-        (t, v) | [tool] <- [ tool | tool <- [minBound..maxBound], low (show tool) == low t ] ->
-          pure (TarballFilter $ Right $ Just tool) <*> makeRegexOptsM compIgnoreCase execBlank (drop 1 v)
-        (t, v) | [tool] <- [ tool | tool <- [minBound..maxBound], low (show tool) == low t ] ->
-          pure (TarballFilter $ Left tool) <*> makeRegexOptsM compIgnoreCase execBlank (drop 1 v)
-        _ -> fail "invalid tool"
-    low = fmap toLower
-
-
-opts :: Parser Options
-opts = Options <$> com
-
-com :: Parser Command
-com = subparser
-  (  command
-       "check"
-       (   ValidateYAML
-       <$> info (validateYAMLOpts <**> helper)
-                (progDesc "Validate the YAML")
-       )
-  <> command
-       "check-tarballs"
-       (info
-         ((ValidateTarballs <$> validateYAMLOpts <*> tarballFilterP) <**> helper)
-         (progDesc "Validate all tarballs (download and checksum)")
-       )
-  )
-
-
-
-main :: IO ()
-main = do
-  no_color <- isJust <$> lookupEnv "NO_COLOR"
-  let loggerConfig = LoggerConfig { lcPrintDebug  = True
-                                  , consoleOutter = T.hPutStr stderr
-                                  , fileOutter    = \_ -> pure ()
-                                  , fancyColors   = not no_color
-                                  }
-  dirs <- liftIO getAllDirs
-  let leanAppstate = LeanAppState (Settings True False Never Curl True GHCupURL False GPGNone False) dirs defaultKeyBindings loggerConfig
-
-  pfreq <- (
-    flip runReaderT leanAppstate . runE @'[NoCompatiblePlatform, NoCompatibleArch, DistroNotFound] $ platformRequest
-    ) >>= \case
-            VRight r -> pure r
-            VLeft e -> do
-              flip runReaderT leanAppstate $ logError $ T.pack $ prettyShow e
-              liftIO $ exitWith (ExitFailure 2)
-
-  let appstate = AppState (Settings True False Never Curl True GHCupURL False GPGNone False) dirs defaultKeyBindings (GHCupInfo mempty mempty mempty) pfreq loggerConfig
-
-  _ <- customExecParser (prefs showHelpOnError) (info (opts <**> helper) idm)
-    >>= \Options {..} -> case optCommand of
-          ValidateYAML vopts -> withValidateYamlOpts vopts (\dl m -> flip runReaderT appstate $ validate dl m)
-          ValidateTarballs vopts tarballFilter -> withValidateYamlOpts vopts (\dl m -> flip runReaderT appstate $ validateTarballs tarballFilter dl m)
-  pure ()
-
- where
-  withValidateYamlOpts vopts f = case vopts of
-    ValidateYAMLOpts { vInput = Nothing } ->
-      B.getContents >>= valAndExit f
-    ValidateYAMLOpts { vInput = Just StdInput } ->
-      B.getContents >>= valAndExit f
-    ValidateYAMLOpts { vInput = Just (FileInput file) } ->
-      B.readFile file >>= valAndExit f
-  valAndExit f contents = do
-    (GHCupInfo _ av gt) <- case Y.decodeEither' contents of
-      Right r -> pure r
-      Left  e -> die (color Red $ displayException e)
-    f av gt
-      >>= exitWith
diff --git a/app/ghcup-gen/Validate.hs b/app/ghcup-gen/Validate.hs
deleted file mode 100644
--- a/app/ghcup-gen/Validate.hs
+++ /dev/null
@@ -1,280 +0,0 @@
-{-# LANGUAGE CPP              #-}
-{-# LANGUAGE DataKinds        #-}
-{-# LANGUAGE FlexibleContexts #-}
-{-# LANGUAGE QuasiQuotes      #-}
-{-# LANGUAGE TemplateHaskell  #-}
-{-# LANGUAGE TypeApplications #-}
-{-# LANGUAGE ViewPatterns     #-}
-{-# LANGUAGE OverloadedStrings #-}
-
-module Validate where
-
-import           GHCup
-import           GHCup.Download
-import           GHCup.Errors
-import           GHCup.Types
-import           GHCup.Types.Optics
-import           GHCup.Utils
-import           GHCup.Utils.Logger
-import           GHCup.Utils.Version.QQ
-
-import           Codec.Archive
-import           Control.Applicative
-import           Control.Exception.Safe
-import           Control.Monad
-import           Control.Monad.IO.Class
-import           Control.Monad.Reader.Class
-import           Control.Monad.Trans.Class      ( lift )
-import           Control.Monad.Trans.Reader     ( runReaderT )
-import           Control.Monad.Trans.Resource   ( runResourceT
-                                                , MonadUnliftIO
-                                                )
-import           Data.Containers.ListUtils      ( nubOrd )
-import           Data.IORef
-import           Data.List
-import           Data.Versions
-import           Haskus.Utils.Variant.Excepts
-import           Optics
-import           System.FilePath
-import           System.Exit
-import           Text.ParserCombinators.ReadP
-import           Text.PrettyPrint.HughesPJClass ( prettyShow )
-import           Text.Regex.Posix
-
-import qualified Data.Map.Strict               as M
-import qualified Data.Text                     as T
-import qualified Data.Version                  as V
-
-
-data ValidationError = InternalError String
-  deriving Show
-
-instance Exception ValidationError
-
-
-addError :: (MonadReader (IORef Int) m, MonadIO m, Monad m) => m ()
-addError = do
-  ref <- ask
-  liftIO $ modifyIORef ref (+ 1)
-
-
-validate :: (Monad m, MonadReader env m, HasLog env, MonadThrow m, MonadIO m, MonadUnliftIO m)
-         => GHCupDownloads
-         -> M.Map GlobalTool DownloadInfo
-         -> m ExitCode
-validate dls _ = do
-  ref <- liftIO $ newIORef 0
-
-  -- verify binary downloads --
-  flip runReaderT ref $ do
-    -- unique tags
-    forM_ (M.toList dls) $ \(t, _) -> checkUniqueTags t
-
-    -- required platforms
-    forM_ (M.toList dls) $ \(t, versions) ->
-      forM_ (M.toList versions) $ \(v, vi) ->
-        forM_ (M.toList $ _viArch vi) $ \(arch, pspecs) -> do
-          checkHasRequiredPlatforms t v (_viTags vi) arch (M.keys pspecs)
-
-    checkGHCVerIsValid
-    forM_ (M.toList dls) $ \(t, _) -> checkMandatoryTags t
-    _ <- checkGHCHasBaseVersion
-
-    -- exit
-    e <- liftIO $ readIORef ref
-    if e > 0
-      then pure $ ExitFailure e
-      else do
-        lift $ logInfo "All good"
-        pure ExitSuccess
- where
-  checkHasRequiredPlatforms t v tags arch pspecs = do
-    let v' = prettyVer v
-        arch' = prettyShow arch
-    when (Linux UnknownLinux `notElem` pspecs) $ do
-      lift $ logError $
-        "Linux UnknownLinux missing for for " <> T.pack (prettyShow t) <> " " <> v' <> " " <> T.pack arch'
-      addError
-    when ((Darwin `notElem` pspecs) && arch == A_64) $ do
-      lift $ logError $ "Darwin missing for for " <> T.pack (prettyShow t) <> " " <> v' <> " " <> T.pack arch'
-      addError
-    when ((FreeBSD `notElem` pspecs) && arch == A_64) $ lift $ logWarn $
-      "FreeBSD missing for for " <> T.pack (prettyShow t) <> " " <> v' <> " " <> T.pack arch'
-    when (Windows `notElem` pspecs && arch == A_64) $ do
-      lift $ logError $ "Windows missing for for " <> T.pack (prettyShow t) <> " " <> v' <> " " <> T.pack arch'
-      addError
-
-    -- alpine needs to be set explicitly, because
-    -- we cannot assume that "Linux UnknownLinux" runs on Alpine
-    -- (although it could be static)
-    when (Linux Alpine `notElem` pspecs) $
-      case t of
-        GHCup | arch `elem` [A_64, A_32] -> lift (logError $ "Linux Alpine missing for " <> T.pack (prettyShow t) <> " " <> v' <> " " <> T.pack (prettyShow arch)) >> addError
-        Cabal | v > [vver|2.4.1.0|]
-              , arch `elem` [A_64, A_32] -> lift (logError $ "Linux Alpine missing for " <> T.pack (prettyShow t) <> " " <> v' <> " " <> T.pack (prettyShow arch)) >> addError
-        GHC | Latest `elem` tags || Recommended `elem` tags
-            , arch `elem` [A_64, A_32] -> lift (logError $ "Linux Alpine missing for " <> T.pack (prettyShow t) <> " " <> v' <> " " <> T.pack (prettyShow arch))
-        _ -> lift $ logWarn $ "Linux Alpine missing for " <> T.pack (prettyShow t) <> " " <> v' <> " " <> T.pack (prettyShow arch)
-
-  checkUniqueTags tool = do
-    let allTags = _viTags =<< M.elems (availableToolVersions dls tool)
-    let nonUnique =
-          fmap fst
-            .   filter (\(_, b) -> not b)
-            <$> ( mapM
-                    (\case
-                      [] -> throwM $ InternalError "empty inner list"
-                      (t : ts) ->
-                        pure $ (t, ) (not (isUniqueTag t) || null ts)
-                    )
-                . group
-                . sort
-                $ allTags
-                )
-    case join nonUnique of
-      [] -> pure ()
-      xs -> do
-        lift $ logError $ "Tags not unique for " <> T.pack (prettyShow tool) <> ": " <> T.pack (prettyShow xs)
-        addError
-   where
-    isUniqueTag Latest         = True
-    isUniqueTag Recommended    = True
-    isUniqueTag Old            = False
-    isUniqueTag Prerelease     = False
-    isUniqueTag (Base       _) = False
-    isUniqueTag (UnknownTag _) = False
-
-  checkGHCVerIsValid = do
-    let ghcVers = toListOf (ix GHC % to M.keys % folded) dls
-    forM_ ghcVers $ \v ->
-      case [ x | (x,"") <- readP_to_S V.parseVersion (T.unpack . prettyVer $ v) ] of
-        [_] -> pure ()
-        _   -> do
-          lift $ logError $ "GHC version " <> prettyVer v <> " is not valid"
-          addError
-
-  -- a tool must have at least one of each mandatory tags
-  checkMandatoryTags tool = do
-    let allTags = _viTags =<< M.elems (availableToolVersions dls tool)
-    forM_ [Latest, Recommended] $ \t -> case t `elem` allTags of
-      False -> do
-        lift $ logError $ "Tag " <> T.pack (prettyShow t) <> " missing from " <> T.pack (prettyShow tool)
-        addError
-      True -> pure ()
-
-  -- all GHC versions must have a base tag
-  checkGHCHasBaseVersion = do
-    let allTags = M.toList $ availableToolVersions dls GHC
-    forM allTags $ \(ver, _viTags -> tags) -> case any isBase tags of
-      False -> do
-        lift $ logError $ "Base tag missing from GHC ver " <> prettyVer ver
-        addError
-      True -> pure ()
-
-  isBase (Base _) = True
-  isBase _        = False
-
-data TarballFilter = TarballFilter
-  { tfTool    :: Either GlobalTool (Maybe Tool)
-  , tfVersion :: Regex
-  }
-
-validateTarballs :: ( Monad m
-                    , MonadReader env m
-                    , HasLog env
-                    , HasDirs env
-                    , HasSettings env
-                    , MonadThrow m
-                    , MonadIO m
-                    , MonadUnliftIO m
-                    , MonadMask m
-                    , Alternative m
-                    , MonadFail m
-                    )
-                 => TarballFilter
-                 -> GHCupDownloads
-                 -> M.Map GlobalTool DownloadInfo
-                 -> m ExitCode
-validateTarballs (TarballFilter etool versionRegex) dls gt = do
-  ref <- liftIO $ newIORef 0
-
-   -- download/verify all tarballs
-  let dlis = either (const []) (\tool -> nubOrd $ dls ^.. each %& indices (maybe (const True) (==) tool) %> each %& indices (matchTest versionRegex . T.unpack . prettyVer) % (viSourceDL % _Just `summing` viArch % each % each % each)) etool
-  let gdlis = nubOrd $ gt ^.. each
-  let allDls = either (const gdlis) (const dlis) etool
-  when (null allDls) $ logError "no tarballs selected by filter" *> runReaderT addError ref
-  forM_ allDls (downloadAll ref)
-
-  -- exit
-  e <- liftIO $ readIORef ref
-  if e > 0
-    then pure $ ExitFailure e
-    else do
-      logInfo "All good"
-      pure ExitSuccess
-
- where
-  downloadAll :: ( MonadUnliftIO m
-                 , MonadIO m
-                 , MonadReader env m
-                 , HasLog env
-                 , HasDirs env
-                 , HasSettings env
-                 , MonadCatch m
-                 , MonadMask m
-                 , MonadThrow m
-                 )
-              => IORef Int
-              -> DownloadInfo
-              -> m ()
-  downloadAll ref dli = do
-    r <- runResourceT
-      . runE @'[DigestError
-               , GPGError
-               , DownloadFailed
-               , UnknownArchive
-               , ArchiveResult
-               ]
-      $ do
-        case etool of
-          Right (Just GHCup) -> do
-            tmpUnpack <- lift mkGhcupTmpDir
-            _ <- liftE $ download (_dlUri dli) Nothing (Just (_dlHash dli)) tmpUnpack Nothing False
-            pure Nothing
-          Right _ -> do
-            p <- liftE $ downloadCached dli Nothing
-            fmap (Just . head . splitDirectories . head)
-              . liftE
-              . getArchiveFiles
-              $ p
-          Left ShimGen -> do
-            tmpUnpack <- lift mkGhcupTmpDir
-            _ <- liftE $ download (_dlUri dli) Nothing (Just (_dlHash dli)) tmpUnpack Nothing False
-            pure Nothing
-    case r of
-      VRight (Just basePath) -> do
-        case _dlSubdir dli of
-          Just (RealDir prel) -> do
-            logInfo
-              $ " verifying subdir: " <> T.pack prel
-            when (basePath /= prel) $ do
-              logError $
-                "Subdir doesn't match: expected " <> T.pack prel <> ", got " <> T.pack basePath
-              runReaderT addError ref
-          Just (RegexDir regexString) -> do
-            logInfo $
-              "verifying subdir (regex): " <> T.pack regexString
-            let regex = makeRegexOpts
-                  compIgnoreCase
-                  execBlank
-                  regexString
-            unless (match regex basePath) $ do
-              logError $
-                "Subdir doesn't match: expected regex " <> T.pack regexString <> ", got " <> T.pack basePath
-              runReaderT addError ref
-          Nothing -> pure ()
-      VRight Nothing -> pure ()
-      VLeft  e -> do
-        logError $
-          "Could not download (or verify hash) of " <> T.pack (show dli) <> ", Error was: " <> T.pack (prettyShow e)
-        runReaderT addError ref
diff --git a/app/ghcup/BrickMain.hs b/app/ghcup/BrickMain.hs
--- a/app/ghcup/BrickMain.hs
+++ b/app/ghcup/BrickMain.hs
@@ -10,6 +10,7 @@
 import           GHCup
 import           GHCup.Download
 import           GHCup.Errors
+import           GHCup.Types.Optics ( getDirs )
 import           GHCup.Types         hiding ( LeanAppState(..) )
 import           GHCup.Utils
 import           GHCup.Utils.Logger
@@ -26,6 +27,9 @@
                                                 )
 import           Codec.Archive
 import           Control.Exception.Safe
+#if !MIN_VERSION_base(4,13,0)
+import           Control.Monad.Fail             ( MonadFail )
+#endif
 import           Control.Monad.Reader
 import           Control.Monad.Trans.Except
 import           Control.Monad.Trans.Resource
@@ -40,6 +44,8 @@
 import           Data.Versions           hiding ( str )
 import           Haskus.Utils.Variant.Excepts
 import           Prelude                 hiding ( appendFile )
+import           System.Directory               ( canonicalizePath )
+import           System.FilePath
 import           System.Exit
 import           System.IO.Unsafe
 import           Text.PrettyPrint.HughesPJClass ( prettyShow )
@@ -48,6 +54,8 @@
 import qualified Data.Text                     as T
 import qualified Graphics.Vty                  as Vty
 import qualified Data.Vector                   as V
+import System.Environment (getExecutablePath)
+import qualified System.Posix.Process          as SPP
 
 
 hiddenTools :: [Tool]
@@ -432,28 +440,43 @@
               ]
 
   run (do
+      ce <- liftIO $ fmap (either (const Nothing) Just) $
+        try @_ @SomeException $ getExecutablePath >>= canonicalizePath
+      dirs <- lift getDirs
       case lTool of
         GHC   -> do
           let vi = getVersionInfo lVer GHC dls
-          liftE $ installGHCBin lVer Nothing False $> vi
+          liftE $ installGHCBin lVer Nothing False $> (vi, dirs, ce)
         Cabal -> do
           let vi = getVersionInfo lVer Cabal dls
-          liftE $ installCabalBin lVer Nothing False $> vi
+          liftE $ installCabalBin lVer Nothing False $> (vi, dirs, ce)
         GHCup -> do
           let vi = snd <$> getLatest dls GHCup
-          liftE $ upgradeGHCup Nothing False $> vi
+          liftE $ upgradeGHCup Nothing False $> (vi, dirs, ce)
         HLS   -> do
           let vi = getVersionInfo lVer HLS dls
-          liftE $ installHLSBin lVer Nothing False $> vi
+          liftE $ installHLSBin lVer Nothing False $> (vi, dirs, ce)
         Stack -> do
           let vi = getVersionInfo lVer Stack dls
-          liftE $ installStackBin lVer Nothing False $> vi
+          liftE $ installStackBin lVer Nothing False $> (vi, dirs, ce)
     )
     >>= \case
-          VRight vi                         -> do
-            forM_ (_viPostInstall =<< vi) $ \msg ->
-              logInfo msg
+          VRight (vi, Dirs{..}, Just ce) -> do
+            forM_ (_viPostInstall =<< vi) $ \msg -> logInfo msg
+            case lTool of
+              GHCup -> do
+                up <- liftIO $ fmap (either (const Nothing) Just)
+                  $ try @_ @SomeException $ canonicalizePath (binDir </> "ghcup" <.> exeExt)
+                when ((normalise <$> up) == Just (normalise ce)) $
+                  -- TODO: track cli arguments of previous invocation
+                  liftIO $ SPP.executeFile ce False ["tui"] Nothing
+                logInfo "Please restart 'ghcup' for the changes to take effect"
+              _ -> pure ()
             pure $ Right ()
+          VRight (vi, _, _) -> do
+            forM_ (_viPostInstall =<< vi) $ \msg -> logInfo msg
+            logInfo "Please restart 'ghcup' for the changes to take effect"
+            pure $ Right ()
           VLeft  (V (AlreadyInstalled _ _)) -> pure $ Right ()
           VLeft (V NoUpdate) -> pure $ Right ()
           VLeft e -> pure $ Left $ prettyShow e <> "\n"
@@ -536,17 +559,7 @@
                                   , fileOutter    = \_ -> pure ()
                                   , fancyColors   = True
                                   }
-  newIORef $ AppState (Settings { cache      = True
-                                , noVerify   = False
-                                , keepDirs   = Never
-                                , downloader = Curl
-                                , verbose    = False
-                                , urlSource  = GHCupURL
-                                , noNetwork  = False
-                                , gpgSetting = GPGNone
-                                , noColor    = False
-                                , ..
-                                })
+  newIORef $ AppState defaultSettings
                       dirs
                       defaultKeyBindings
                       (GHCupInfo mempty mempty mempty)
@@ -605,4 +618,3 @@
   flip runReaderT settings $ do
     lV <- listVersions Nothing Nothing
     pure $ BrickData (reverse lV)
-
diff --git a/app/ghcup/GHCup/OptParse.hs b/app/ghcup/GHCup/OptParse.hs
--- a/app/ghcup/GHCup/OptParse.hs
+++ b/app/ghcup/GHCup/OptParse.hs
@@ -67,6 +67,7 @@
   -- global options
     optVerbose     :: Maybe Bool
   , optCache       :: Maybe Bool
+  , optMetaCache   :: Maybe Integer
   , optUrlSource   :: Maybe URI
   , optNoVerify    :: Maybe Bool
   , optKeepDirs    :: Maybe KeepDirs
@@ -105,6 +106,7 @@
   Options
     <$> invertableSwitch "verbose" 'v' False (help "Enable verbosity (default: disabled)")
     <*> invertableSwitch "cache" 'c' False (help "Cache downloads in ~/.ghcup/cache (default: disabled)")
+    <*> optional (option auto (long "metadata-caching" <> help "How long the yaml metadata caching interval is (in seconds), 0 to disable" <> internal))
     <*> optional
           (option
             (eitherReader parseUri)
diff --git a/app/ghcup/GHCup/OptParse/Common.hs b/app/ghcup/GHCup/OptParse/Common.hs
--- a/app/ghcup/GHCup/OptParse/Common.hs
+++ b/app/ghcup/GHCup/OptParse/Common.hs
@@ -89,6 +89,18 @@
   mv _          = "VERSION|TAG"
 
 
+toolVersionOption :: Maybe ListCriteria -> Maybe Tool -> Parser ToolVersion
+toolVersionOption criteria tool =
+  option (eitherReader toolVersionEither)
+    (  sh tool
+    <> completer (tagCompleter (fromMaybe GHC tool) [])
+    <> foldMap (completer . versionCompleter criteria) tool)
+ where
+  sh (Just GHC) = long "ghc" <> metavar "GHC_VERSION|TAG"
+  sh (Just HLS) = long "hls" <> metavar "HLS_VERSION|TAG"
+  sh _          = long "version" <> metavar "VERSION|TAG"
+
+
 versionParser :: Parser GHCTargetVersion
 versionParser = option
   (eitherReader tVersionEither)
@@ -196,8 +208,8 @@
     ]
 
 
-bindistParser :: String -> Either String URI
-bindistParser = first show . parseURI strictURIParserOptions . UTF8.fromString
+uriParser :: String -> Either String URI
+uriParser = first show . parseURI strictURIParserOptions . UTF8.fromString
 
 
 absolutePathParser :: FilePath -> Either String FilePath
@@ -246,19 +258,7 @@
   where t = T.toLower (T.pack s')
 
 
-toolVersionParser :: Parser ToolVersion
-toolVersionParser = verP' <|> toolP
- where
-  verP' = ToolVersion <$> versionParser
-  toolP =
-    ToolTag
-      <$> option
-            (eitherReader tagEither)
-            (short 't' <> long "tag" <> metavar "TAG" <> help "The target tag")
 
-
-
-
 keepOnParser :: String -> Either String KeepDirs
 keepOnParser s' | t == T.pack "always" = Right Always
                 | t == T.pack "errors" = Right Errors
@@ -299,7 +299,7 @@
         , fancyColors    = False
         }
   let appState = LeanAppState
-        (Settings True False Never Curl False GHCupURL True GPGNone False)
+        (defaultSettings { noNetwork = True })
         dirs'
         defaultKeyBindings
         loggerConfig
@@ -322,7 +322,7 @@
         , fileOutter     = mempty
         , fancyColors    = False
         }
-  let settings = Settings True False Never Curl False GHCupURL True GPGNone False
+  let settings = defaultSettings { noNetwork = True }
   let leanAppState = LeanAppState
                    settings
                    dirs'
@@ -399,7 +399,7 @@
     Right pvpIn ->
       lift (getLatestToolFor tool pvpIn dls) >>= \case
         Just (pvp_, vi') -> do
-          v' <- lift $ pvpToVersion pvp_
+          v' <- lift $ pvpToVersion pvp_ ""
           when (v' /= _tvVersion v) $ lift $ logWarn ("Assuming you meant version " <> prettyVer v')
           pure (GHCTargetVersion (_tvTarget v) v', Just vi')
         Nothing -> pure (v, vi)
@@ -472,42 +472,22 @@
                    , MonadIO m
                    , MonadFail m
                    )
-                => m ()
+                => m [(Tool, Version)]
 checkForUpdates = do
   GHCupInfo { _ghcupDownloads = dls } <- getGHCupInfo
   lInstalled <- listVersions Nothing (Just ListInstalled)
   let latestInstalled tool = (fmap lVer . lastMay . filter (\lr -> lTool lr == tool)) lInstalled
 
-  forM_ (getLatest dls GHCup) $ \(l, _) -> do
-    (Right ghc_ver) <- pure $ version $ prettyPVP ghcUpVer
-    when (l > ghc_ver)
-      $ logWarn $
-          "New GHCup version available: " <> prettyVer l <> ". To upgrade, run 'ghcup upgrade'"
-
-  forM_ (getLatest dls GHC) $ \(l, _) -> do
-    let mghc_ver = latestInstalled GHC
-    forM mghc_ver $ \ghc_ver ->
-      when (l > ghc_ver)
-        $ logWarn $
-          "New GHC version available: " <> prettyVer l <> ". To upgrade, run 'ghcup install ghc " <> prettyVer l <> "'"
-
-  forM_ (getLatest dls Cabal) $ \(l, _) -> do
-    let mcabal_ver = latestInstalled Cabal
-    forM mcabal_ver $ \cabal_ver ->
-      when (l > cabal_ver)
-        $ logWarn $
-          "New Cabal version available: " <> prettyVer l <> ". To upgrade, run 'ghcup install cabal " <> prettyVer l <> "'"
+  ghcup <- forMM (getLatest dls GHCup) $ \(l, _) -> do
+    (Right ghcup_ver) <- pure $ version $ prettyPVP ghcUpVer
+    if (l > ghcup_ver) then pure $ Just (GHCup, l) else pure Nothing
 
-  forM_ (getLatest dls HLS) $ \(l, _) -> do
-    let mhls_ver = latestInstalled HLS
-    forM mhls_ver $ \hls_ver ->
-      when (l > hls_ver)
-        $ logWarn $
-          "New HLS version available: " <> prettyVer l <> ". To upgrade, run 'ghcup install hls " <> prettyVer l <> "'"
+  otherTools <- forM [GHC, Cabal, HLS, Stack] $ \t ->
+    forMM (getLatest dls t) $ \(l, _) -> do
+      let mver = latestInstalled t
+      forMM mver $ \ver ->
+        if (l > ver) then pure $ Just (t, l) else pure Nothing
 
-  forM_ (getLatest dls Stack) $ \(l, _) -> do
-    let mstack_ver = latestInstalled Stack
-    forM mstack_ver $ \stack_ver ->
-      when (l > stack_ver)
-        $ logWarn $
-          "New Stack version available: " <> prettyVer l <> ". To upgrade, run 'ghcup install stack " <> prettyVer l <> "'"
+  pure $ catMaybes (ghcup:otherTools)
+ where
+  forMM a f = fmap join $ forM a f
diff --git a/app/ghcup/GHCup/OptParse/Compile.hs b/app/ghcup/GHCup/OptParse/Compile.hs
--- a/app/ghcup/GHCup/OptParse/Compile.hs
+++ b/app/ghcup/GHCup/OptParse/Compile.hs
@@ -40,6 +40,7 @@
 import           System.Exit
 import           Text.PrettyPrint.HughesPJClass ( prettyShow )
 
+import           URI.ByteString          hiding ( uriParser )
 import qualified Data.Text                     as T
 import Control.Exception.Safe (MonadMask)
 import System.FilePath (isPathSeparator)
@@ -68,7 +69,7 @@
   , bootstrapGhc :: Either Version FilePath
   , jobs         :: Maybe Int
   , buildConfig  :: Maybe FilePath
-  , patchDir     :: Maybe FilePath
+  , patches      :: Maybe (Either FilePath [URI])
   , crossTarget  :: Maybe Text
   , addConfArgs  :: [Text]
   , setCompile   :: Bool
@@ -84,10 +85,11 @@
   , setCompile   :: Bool
   , ovewrwiteVer :: Maybe Version
   , isolateDir   :: Maybe FilePath
-  , cabalProject :: Maybe FilePath
-  , cabalProjectLocal :: Maybe FilePath
-  , patchDir     :: Maybe FilePath
+  , cabalProject :: Maybe (Either FilePath URI)
+  , cabalProjectLocal :: Maybe URI
+  , patches      :: Maybe (Either FilePath [URI])
   , targetGHCs   :: [ToolVersion]
+  , cabalArgs    :: [Text]
   }
 
 
@@ -148,7 +150,10 @@
   These need to be available in PATH prior to compilation.
 
 Examples:
-  ghcup compile hls -v 1.4.0 -j 12 8.10.5 8.10.7 9.0.1|]
+  # compile 1.4.0 for ghc 8.10.5 and 8.10.7
+  ghcup compile hls -v 1.4.0 -j 12 --ghc 8.10.5 --ghc 8.10.7
+  # compile from master for ghc 8.10.7, linking everything dynamically
+  ghcup compile hls -g master -j 12 --ghc 8.10.7 -- --ghc-options='-dynamic'|]
 
 
 ghcCompileOpts :: Parser GHCCompileOptions
@@ -195,13 +200,23 @@
               "Absolute path to build config file"
             )
           )
-    <*> optional
-          (option
-            str
-            (short 'p' <> long "patchdir" <> metavar "PATCH_DIR" <> help
-              "Absolute path to patch directory (applies all .patch and .diff files in order using -p1)"
+    <*> (optional
+          (
+            (fmap Right $ many $ option
+              (eitherReader uriParser)
+              (long "patch" <> metavar "PATCH_URI" <> help
+                "URI to a patch (https/http/file)"
+              )
             )
+            <|>
+            (fmap Left $ option
+              str
+              (short 'p' <> long "patchdir" <> metavar "PATCH_DIR" <> help
+                "Absolute path to patch directory (applies all .patch and .diff files in order using -p1)"
+              )
+            )
           )
+        )
     <*> optional
           (option
             str
@@ -296,26 +311,37 @@
            )
     <*> optional
           (option
-            str
+            ((fmap Right $ eitherReader uriParser) <|> (fmap Left str))
             (long "cabal-project" <> metavar "CABAL_PROJECT" <> help
-              "If relative, specifies the path to cabal.project inside the unpacked HLS tarball/checkout. If absolute, will copy the file over."
+              "If relative filepath, specifies the path to cabal.project inside the unpacked HLS tarball/checkout. Otherwise expects a full URI with https/http/file scheme."
             )
           )
     <*> optional
           (option
-            (eitherReader absolutePathParser)
+            (eitherReader uriParser)
             (long "cabal-project-local" <> metavar "CABAL_PROJECT_LOCAL" <> help
-              "Absolute path to a cabal.project.local to be used for the build. Will be copied over."
+              "URI (https/http/file) to a cabal.project.local to be used for the build. Will be copied over."
             )
           )
-    <*> optional
-          (option
-            (eitherReader absolutePathParser)
-            (short 'p' <> long "patchdir" <> metavar "PATCH_DIR" <> help
-              "Absolute path to patch directory (applies all .patch and .diff files in order using -p1)"
+    <*> (optional
+          (
+            (fmap Right $ many $ option
+              (eitherReader uriParser)
+              (long "patch" <> metavar "PATCH_URI" <> help
+                "URI to a patch (https/http/file)"
+              )
             )
+            <|>
+            (fmap Left $ option
+              str
+              (short 'p' <> long "patchdir" <> metavar "PATCH_DIR" <> help
+                "Absolute path to patch directory (applies all .patch and .diff files in order using -p1)"
+              )
+            )
           )
-    <*> some (toolVersionArgument Nothing (Just GHC))
+        )
+    <*> some (toolVersionOption Nothing (Just GHC))
+    <*> many (argument str (metavar "CABAL_ARGS" <> help "Additional arguments to cabal install, prefix with '-- ' (longopts)"))
 
 
 
@@ -403,11 +429,11 @@
            )
       => CompileCommand
       -> Settings
+      -> Dirs
       -> (forall eff a . ReaderT AppState m (VEither eff a) -> m (VEither eff a))
       -> (ReaderT LeanAppState m () -> m ())
       -> m ExitCode
-compile compileCommand settings runAppState runLogger = do
-  VRight Dirs{ .. }  <- runAppState (VRight <$> getDirs)
+compile compileCommand settings Dirs{..} runAppState runLogger = do
   case compileCommand of
     (CompileHLS HLSCompileOptions { .. }) -> do
       runCompileHLS runAppState (do
@@ -430,7 +456,8 @@
                     isolateDir
                     cabalProject
                     cabalProjectLocal
-                    patchDir
+                    patches
+                    cabalArgs
         GHCupInfo { _ghcupDownloads = dls } <- lift getGHCupInfo
         let vi = getVersionInfo targetVer HLS dls
         when setCompile $ void $ liftE $
@@ -477,7 +504,7 @@
                     bootstrapGhc
                     jobs
                     buildConfig
-                    patchDir
+                    patches
                     addConfArgs
                     buildFlavour
                     hadrian
diff --git a/app/ghcup/GHCup/OptParse/Config.hs b/app/ghcup/GHCup/OptParse/Config.hs
--- a/app/ghcup/GHCup/OptParse/Config.hs
+++ b/app/ghcup/GHCup/OptParse/Config.hs
@@ -122,6 +122,7 @@
    mergeConf :: UserSettings -> Settings -> Settings
    mergeConf UserSettings{..} Settings{..} =
      let cache'      = fromMaybe cache uCache
+         metaCache'  = fromMaybe metaCache uMetaCache
          noVerify'   = fromMaybe noVerify uNoVerify
          keepDirs'   = fromMaybe keepDirs uKeepDirs
          downloader' = fromMaybe downloader uDownloader
@@ -129,7 +130,7 @@
          urlSource'  = fromMaybe urlSource uUrlSource
          noNetwork'  = fromMaybe noNetwork uNoNetwork
          gpgSetting' = fromMaybe gpgSetting uGPGSetting
-     in Settings cache' noVerify' keepDirs' downloader' verbose' urlSource' noNetwork' gpgSetting' noColor
+     in Settings cache' metaCache' noVerify' keepDirs' downloader' verbose' urlSource' noNetwork' gpgSetting' noColor
 
 
 
diff --git a/app/ghcup/GHCup/OptParse/DInfo.hs b/app/ghcup/GHCup/OptParse/DInfo.hs
--- a/app/ghcup/GHCup/OptParse/DInfo.hs
+++ b/app/ghcup/GHCup/OptParse/DInfo.hs
@@ -51,7 +51,7 @@
                      runIO (do
                              CapturedProcess{..} <-  do
                               dirs <- liftIO getAllDirs
-                              let settings = AppState (Settings True False Never Curl False GHCupURL False GPGNone False)
+                              let settings = AppState (defaultSettings { noNetwork = True })
                                                dirs
                                                defaultKeyBindings
                               flip runReaderT settings $ executeOut "git" ["describe"] Nothing
diff --git a/app/ghcup/GHCup/OptParse/Install.hs b/app/ghcup/GHCup/OptParse/Install.hs
--- a/app/ghcup/GHCup/OptParse/Install.hs
+++ b/app/ghcup/GHCup/OptParse/Install.hs
@@ -37,7 +37,7 @@
 import           Prelude                 hiding ( appendFile )
 import           System.Exit
 import           Text.PrettyPrint.HughesPJClass ( prettyShow )
-import           URI.ByteString
+import           URI.ByteString          hiding ( uriParser )
 
 import qualified Data.Text                     as T
 
@@ -187,7 +187,7 @@
     <*> (   (   (,)
             <$> optional
                   (option
-                    (eitherReader bindistParser)
+                    (eitherReader uriParser)
                     (short 'u' <> long "url" <> metavar "BINDIST_URL" <> help
                       "Install the specified version from this bindist"
                     )
diff --git a/app/ghcup/Main.hs b/app/ghcup/Main.hs
--- a/app/ghcup/Main.hs
+++ b/app/ghcup/Main.hs
@@ -20,6 +20,7 @@
 import           GHCup.Errors
 import           GHCup.Platform
 import           GHCup.Types
+import           GHCup.Types.Optics      hiding ( toolRequirements )
 import           GHCup.Utils
 import           GHCup.Utils.Logger
 import           GHCup.Utils.Prelude
@@ -39,6 +40,7 @@
 import           Data.Either
 import           Data.Functor
 import           Data.Maybe
+import           Data.Versions
 import           GHC.IO.Encoding
 import           Haskus.Utils.Variant.Excepts
 import           Language.Haskell.TH
@@ -55,6 +57,7 @@
 import qualified Data.Text                     as T
 import qualified Data.Text.IO                  as T
 import qualified Data.Text.Encoding            as E
+import qualified GHCup.Types                   as Types
 
 
 
@@ -72,15 +75,16 @@
  where
    mergeConf :: Options -> UserSettings -> Bool -> (Settings, KeyBindings)
    mergeConf Options{..} UserSettings{..} noColor =
-     let cache       = fromMaybe (fromMaybe False uCache) optCache
-         noVerify    = fromMaybe (fromMaybe False uNoVerify) optNoVerify
-         verbose     = fromMaybe (fromMaybe False uVerbose) optVerbose
-         keepDirs    = fromMaybe (fromMaybe Errors uKeepDirs) optKeepDirs
+     let cache       = fromMaybe (fromMaybe (Types.cache defaultSettings) uCache) optCache
+         metaCache   = fromMaybe (fromMaybe (Types.metaCache defaultSettings) uMetaCache) optMetaCache
+         noVerify    = fromMaybe (fromMaybe (Types.noVerify defaultSettings) uNoVerify) optNoVerify
+         verbose     = fromMaybe (fromMaybe (Types.verbose defaultSettings) uVerbose) optVerbose
+         keepDirs    = fromMaybe (fromMaybe (Types.keepDirs defaultSettings) uKeepDirs) optKeepDirs
          downloader  = fromMaybe (fromMaybe defaultDownloader uDownloader) optsDownloader
          keyBindings = maybe defaultKeyBindings mergeKeys uKeyBindings
-         urlSource   = maybe (fromMaybe GHCupURL uUrlSource) OwnSource optUrlSource
-         noNetwork   = fromMaybe (fromMaybe False uNoNetwork) optNoNetwork
-         gpgSetting  = fromMaybe (fromMaybe GPGNone uGPGSetting) optGpg
+         urlSource   = maybe (fromMaybe (Types.urlSource defaultSettings) uUrlSource) OwnSource optUrlSource
+         noNetwork   = fromMaybe (fromMaybe (Types.noNetwork defaultSettings) uNoNetwork) optNoNetwork
+         gpgSetting  = fromMaybe (fromMaybe (Types.gpgSetting defaultSettings) uGPGSetting) optGpg
      in (Settings {..}, keyBindings)
 #if defined(INTERNAL_DOWNLOADER)
    defaultDownloader = Internal
@@ -189,7 +193,7 @@
           -------------------------
 
 
-              appState = do
+          let appState = do
                 pfreq <- (
                   runLogger . runE @'[NoCompatiblePlatform, NoCompatibleArch, DistroNotFound] . liftE $ platformRequest
                   ) >>= \case
@@ -225,8 +229,28 @@
 #if defined(BRICK)
                   Interactive -> pure ()
 #endif
+                  -- check for new tools
                   _ -> lookupEnv "GHCUP_SKIP_UPDATE_CHECK" >>= \case
-                         Nothing -> runReaderT checkForUpdates s'
+                         Nothing -> void . flip runReaderT s' . runE @'[TagNotFound, NextVerNotFound, NoToolVersionSet] $ do
+                           newTools <- lift checkForUpdates 
+                           forM_ newTools $ \newTool@(t, l) -> do
+                             -- https://gitlab.haskell.org/haskell/ghcup-hs/-/issues/283
+                             alreadyInstalling' <- alreadyInstalling optCommand newTool
+                             when (not alreadyInstalling') $
+                               case t of
+                                 GHCup -> runLogger $
+                                            logWarn ("New GHCup version available: "
+                                              <> prettyVer l
+                                              <> ". To upgrade, run 'ghcup upgrade'")
+                                 _ -> runLogger $
+                                        logWarn ("New "
+                                          <> T.pack (prettyShow t)
+                                          <> " version available. "
+                                          <> "To upgrade, run 'ghcup install "
+                                          <> T.pack (prettyShow t)
+                                          <> " "
+                                          <> prettyVer l
+                                          <> "'")
                          Just _ -> pure ()
 
                 -- TODO: always run for windows
@@ -268,7 +292,7 @@
             List lo                  -> list lo no_color runAppState
             Rm rmCommand             -> rm rmCommand runAppState runLogger
             DInfo                    -> dinfo runAppState runLogger
-            Compile compileCommand   -> compile compileCommand settings runAppState runLogger
+            Compile compileCommand   -> compile compileCommand settings dirs runAppState runLogger
             Config configCommand     -> config configCommand settings keybindings runLogger
             Whereis whereisOptions
                     whereisCommand   -> whereis whereisCommand whereisOptions runAppState leanAppstate runLogger
@@ -285,4 +309,55 @@
 
   pure ()
 
+ where
+  alreadyInstalling :: ( HasLog env
+                       , MonadFail m
+                       , MonadReader env m
+                       , HasGHCupInfo env
+                       , HasDirs env
+                       , MonadThrow m
+                       , MonadIO m
+                       , MonadCatch m
+                       )
+                    => Command
+                    -> (Tool, Version)
+                    -> Excepts
+                         '[ TagNotFound
+                          , NextVerNotFound
+                          , NoToolVersionSet
+                          ] m Bool
+  alreadyInstalling (Install (Right InstallOptions{..}))                 (GHC, ver)   = cmp' GHC instVer ver
+  alreadyInstalling (Install (Left (InstallGHC InstallOptions{..})))     (GHC, ver)   = cmp' GHC instVer ver
+  alreadyInstalling (Install (Left (InstallCabal InstallOptions{..})))   (Cabal, ver) = cmp' Cabal instVer ver
+  alreadyInstalling (Install (Left (InstallHLS InstallOptions{..})))     (HLS, ver)   = cmp' HLS instVer ver
+  alreadyInstalling (Install (Left (InstallStack InstallOptions{..})))   (Stack, ver) = cmp' Stack instVer ver
+  alreadyInstalling (Compile (CompileGHC GHCCompileOptions{ ovewrwiteVer = Just over }))
+    (GHC, ver)   = cmp' GHC (Just $ ToolVersion (mkTVer over)) ver
+  alreadyInstalling (Compile (CompileGHC GHCCompileOptions{ targetGhc = Left tver }))
+    (GHC, ver)   = cmp' GHC (Just $ ToolVersion (mkTVer tver)) ver
+  alreadyInstalling (Compile (CompileHLS HLSCompileOptions{ ovewrwiteVer = Just over }))
+    (HLS, ver)   = cmp' HLS (Just $ ToolVersion (mkTVer over)) ver
+  alreadyInstalling (Compile (CompileHLS HLSCompileOptions{ targetHLS = Left tver }))
+    (HLS, ver)   = cmp' HLS (Just $ ToolVersion (mkTVer tver)) ver
+  alreadyInstalling _ _ = pure False
 
+  cmp' :: ( HasLog env
+          , MonadFail m
+          , MonadReader env m
+          , HasGHCupInfo env
+          , HasDirs env
+          , MonadThrow m
+          , MonadIO m
+          , MonadCatch m
+          )
+       => Tool
+       -> Maybe ToolVersion
+       -> Version
+       -> Excepts
+            '[ TagNotFound
+             , NextVerNotFound
+             , NoToolVersionSet
+             ] m Bool
+  cmp' tool instVer ver = do
+    (v, _) <- liftE $ fromVersion instVer tool
+    pure (v == mkTVer ver)
diff --git a/data/config.yaml b/data/config.yaml
--- a/data/config.yaml
+++ b/data/config.yaml
@@ -36,6 +36,10 @@
   show-all-tools:
     KChar: 't'
 
+# The caching for the metadata files containing download info, depending on last access time
+# of the file. These usually are in '~/.ghcup/cache/ghcup-<ver>.yaml'.
+meta-cache: 300 # in seconds
+
 # Where to get GHC/cabal/hls download info/versions from. For more detailed explanation
 # check the 'URLSource' type in the code.
 url-source:
diff --git a/ghcup.cabal b/ghcup.cabal
--- a/ghcup.cabal
+++ b/ghcup.cabal
@@ -1,6 +1,6 @@
 cabal-version:      3.0
 name:               ghcup
-version:            0.1.17.3
+version:            0.1.17.4
 license:            LGPL-3.0-only
 license-file:       LICENSE
 copyright:          Julian Ospald 2020
@@ -43,6 +43,11 @@
   default:     False
   manual:      True
 
+flag no-exe
+  description: Don't build any executables 
+  default:     False
+  manual:      True
+
 library
   exposed-modules:
     GHCup
@@ -91,7 +96,7 @@
   build-depends:
     , aeson                 >=1.4
     , async                 >=0.8        && <2.3
-    , base                  >=4.13       && <5
+    , base                  >=4.12       && <5
     , base16-bytestring     >=0.1.1.6    && <1.1
     , binary                ^>=0.8.6.0
     , bytestring            ^>=0.10
@@ -105,7 +110,7 @@
     , disk-free-space       ^>=0.1.0.1
     , filepath              ^>=1.4.2.1
     , haskus-utils-types    ^>=1.5
-    , haskus-utils-variant  >=3.0        && <3.2
+    , haskus-utils-variant  ^>=3.2.1
     , libarchive            ^>=3.0.3.0
     , lzma-static           ^>=5.2.5.3
     , megaparsec            >=8.0.0      && <9.1
@@ -214,14 +219,15 @@
     , aeson                 >=1.4
     , aeson-pretty          ^>=0.8.8
     , async                 ^>=2.2.3
-    , base                  >=4.13     && <5
+    , base                  >=4.12     && <5
     , bytestring            ^>=0.10
     , cabal-plan            ^>=0.7.2
     , containers            ^>=0.6
     , deepseq               ^>=1.4
+    , directory             ^>=1.3.6.0
     , filepath              ^>=1.4.2.1
     , ghcup
-    , haskus-utils-variant  >=3.0      && <3.2
+    , haskus-utils-variant  ^>=3.2.1
     , libarchive            ^>=3.0.3.0
     , megaparsec            >=8.0.0    && <9.1
     , mtl                   ^>=2.2
@@ -247,56 +253,15 @@
     build-depends:
       , brick         ^>=0.64
       , transformers  ^>=0.5
+      , unix          ^>=2.7
       , vector        ^>=0.12
       , vty           >=5.28.2 && <5.34
 
   if os(windows)
     cpp-options: -DIS_WINDOWS
 
-executable ghcup-gen
-  main-is:            Main.hs
-  hs-source-dirs:     app/ghcup-gen
-  other-modules:      Validate
-  default-language:   Haskell2010
-  default-extensions:
-    DeriveGeneric
-    LambdaCase
-    MultiWayIf
-    NamedFieldPuns
-    PackageImports
-    QuasiQuotes
-    RecordWildCards
-    ScopedTypeVariables
-    StrictData
-    TupleSections
-    TypeApplications
-    TypeFamilies
-    ViewPatterns
-
-  ghc-options:
-    -Wall -fwarn-tabs -fwarn-incomplete-uni-patterns
-    -fwarn-incomplete-record-updates -threaded
-
-  build-depends:
-    , base                  >=4.13     && <5
-    , bytestring            ^>=0.10
-    , containers            ^>=0.6
-    , filepath              ^>=1.4.2.1
-    , ghcup
-    , haskus-utils-variant  >=3.0      && <3.2
-    , libarchive            ^>=3.0.3.0
-    , mtl                   ^>=2.2
-    , optics                ^>=0.4
-    , optparse-applicative  >=0.15.1.0 && <0.17
-    , pretty                ^>=1.1.3.1
-    , pretty-terminal       ^>=0.1.0.0
-    , regex-posix           ^>=0.96
-    , resourcet             ^>=1.2.2
-    , safe-exceptions       ^>=0.1
-    , text                  ^>=1.2.4.0
-    , transformers          ^>=0.5
-    , versions              >=4.0.1    && <5.1
-    , yaml-streamly         ^>=0.12.0
+  if flag(no-exe)
+    buildable: False
 
 test-suite ghcup-test
   type:               exitcode-stdio-1.0
@@ -322,7 +287,7 @@
     -fwarn-incomplete-record-updates
 
   build-depends:
-    , base                      >=4.13    && <5
+    , base                      >=4.12    && <5
     , bytestring                ^>=0.10
     , containers                ^>=0.6
     , generic-arbitrary         ^>=0.1.0
diff --git a/lib/GHCup.hs b/lib/GHCup.hs
--- a/lib/GHCup.hs
+++ b/lib/GHCup.hs
@@ -62,7 +62,7 @@
 import           Data.Text                      ( Text )
 import           Data.Time.Clock
 import           Data.Time.Format.ISO8601
-import           Data.Versions
+import           Data.Versions                hiding ( patch )
 import           Distribution.Types.Version   hiding ( Version )
 import           Distribution.Types.PackageId
 import           Distribution.Types.PackageDescription
@@ -84,6 +84,7 @@
 import           System.IO.Temp
 import           Text.PrettyPrint.HughesPJClass ( prettyShow )
 import           Text.Regex.Posix
+import           URI.ByteString
 
 import qualified Crypto.Hash.SHA256            as SHA256
 import qualified Data.List.NonEmpty            as NE
@@ -750,9 +751,10 @@
            -> Maybe Int
            -> Maybe Version
            -> Maybe FilePath
-           -> Maybe FilePath
-           -> Maybe FilePath
-           -> Maybe FilePath
+           -> Maybe (Either FilePath URI)
+           -> Maybe URI
+           -> Maybe (Either FilePath [URI])  -- ^ patches
+           -> [Text]                   -- ^ additional args to cabal install
            -> Excepts '[ NoDownload
                        , GPGError
                        , DownloadFailed
@@ -763,11 +765,12 @@
                        , BuildFailed
                        , NotInstalled
                        ] m Version
-compileHLS targetHLS ghcs jobs ov isolateDir cabalProject cabalProjectLocal patchdir = do
+compileHLS targetHLS ghcs jobs ov isolateDir cabalProject cabalProjectLocal patches cabalArgs = do
   PlatformRequest { .. } <- lift getPlatformReq
   GHCupInfo { _ghcupDownloads = dls } <- lift getGHCupInfo
   Dirs { .. } <- lift getDirs
 
+
   (workdir, tver) <- case targetHLS of
     -- unpack from version tarball
     Left tver -> do
@@ -834,48 +837,51 @@
   liftE $ runBuildAction
     workdir
     Nothing
-    (reThrowAll @_ @'[PatchFailed, ProcessError, FileAlreadyExistsError, CopyError] @'[BuildFailed] (BuildFailed workdir) $ do
+    (reThrowAll @_ @'[GPGError, DownloadFailed, DigestError, PatchFailed, ProcessError, FileAlreadyExistsError, CopyError] @'[BuildFailed] (BuildFailed workdir) $ do
       let installDir = workdir </> "out"
       liftIO $ createDirRecursive' installDir
 
       -- apply patches
-      forM_ patchdir (\dir -> liftE $ applyPatches dir workdir)
+      liftE $ applyAnyPatch patches workdir
 
       -- set up project files
       cp <- case cabalProject of
-        Just cp
+        Just (Left cp)
           | isAbsolute cp -> do
               copyFileE cp (workdir </> "cabal.project")
               pure "cabal.project"
           | otherwise -> pure (takeFileName cp)
+        Just (Right uri) -> do
+          tmpUnpack <- lift withGHCupTmpDir
+          cp <- liftE $ download uri Nothing Nothing tmpUnpack (Just "cabal.project") False
+          copyFileE cp (workdir </> "cabal.project")
+          pure "cabal.project"
         Nothing -> pure "cabal.project"
-      forM_ cabalProjectLocal $ \cpl -> copyFileE cpl (workdir </> cp <.> "local")
-
-      let targets = ["exe:haskell-language-server", "exe:haskell-language-server-wrapper"]
-
+      forM_ cabalProjectLocal $ \uri -> do
+        tmpUnpack <- lift withGHCupTmpDir
+        cpl <- liftE $ download uri Nothing Nothing tmpUnpack (Just (cp <.> "local")) False
+        copyFileE cpl (workdir </> cp <.> "local")
       artifacts <- forM (sort ghcs) $ \ghc -> do
         let ghcInstallDir = installDir </> T.unpack (prettyVer ghc)
-        liftIO $ createDirRecursive' ghcInstallDir
+        liftIO $ createDirRecursive' installDir
         lift $ logInfo $ "Building HLS " <> prettyVer installVer <> " for GHC version " <> prettyVer ghc
         liftE $ lEM @_ @'[ProcessError] $
-          execLogged "cabal" ( [ "v2-build"
+          execLogged "cabal" ( [ "v2-install"
                                , "-w"
                                , "ghc-" <> T.unpack (prettyVer ghc)
+                               , "--install-method=copy"
                                ] ++
                                maybe [] (\j -> ["--jobs=" <> show j]) jobs ++
-                               [ "--project-file=" <> cp
-                               ] ++ targets
+                               [ "--overwrite-policy=always"
+                               , "--disable-profiling"
+                               , "--disable-tests"
+                               , "--installdir=" <> ghcInstallDir
+                               , "--project-file=" <> cp
+                               ] ++ fmap T.unpack cabalArgs ++ [
+                                 "exe:haskell-language-server"
+                               , "exe:haskell-language-server-wrapper"]
                              )
           (Just workdir) "cabal" Nothing
-        forM_ targets $ \target -> do
-          let cabal = "cabal"
-              args = ["list-bin", target]
-          CapturedProcess{..} <- lift $ executeOut cabal args  (Just workdir) 
-          case _exitCode of
-            ExitFailure i -> throwE (NonZeroExit i cabal args)
-            _ -> pure ()
-          let cbin = stripNewlineEnd . T.unpack . decUTF8Safe' $ _stdOut
-          copyFileE cbin (ghcInstallDir </> takeFileName cbin)
         pure ghcInstallDir
 
       forM_ artifacts $ \artifact -> do
@@ -1102,7 +1108,7 @@
         pure $ Just (file <> "-" <> verS)
 
     -- create symlink
-    forM mTargetFile $ \targetFile -> do
+    forM_ mTargetFile $ \targetFile -> do
       let fullF = binDir </> targetFile  <> exeExt
           fileWithExt = file <> exeExt
       destL <- lift $ ghcLinkDestination fileWithExt ver
@@ -1574,7 +1580,7 @@
 
   currentGHCup :: Map.Map Version VersionInfo -> Maybe ListResult
   currentGHCup av =
-    let currentVer = fromJust $ pvpToVersion ghcUpVer
+    let currentVer = fromJust $ pvpToVersion ghcUpVer ""
         listVer    = Map.lookup currentVer av
         latestVer  = fst <$> headOf (getTagged Latest) av
         recommendedVer = fst <$> headOf (getTagged Latest) av
@@ -2090,7 +2096,7 @@
            -> Either Version FilePath  -- ^ version to bootstrap with
            -> Maybe Int                -- ^ jobs
            -> Maybe FilePath           -- ^ build config
-           -> Maybe FilePath           -- ^ patch directory
+           -> Maybe (Either FilePath [URI])  -- ^ patches
            -> [Text]                   -- ^ additional args to ./configure
            -> Maybe String             -- ^ build flavour
            -> Bool
@@ -2119,7 +2125,7 @@
                  ]
                 m
                 GHCTargetVersion
-compileGHC targetGhc ov bstrap jobs mbuildConfig patchdir aargs buildFlavour hadrian isolateDir
+compileGHC targetGhc ov bstrap jobs mbuildConfig patches aargs buildFlavour hadrian isolateDir
   = do
     PlatformRequest { .. } <- lift getPlatformReq
     GHCupInfo { _ghcupDownloads = dls } <- lift getGHCupInfo
@@ -2143,7 +2149,7 @@
         workdir <- maybe (pure tmpUnpack)
                          (liftE . intoSubdir tmpUnpack)
                          (view dlSubdir dlInfo)
-        forM_ patchdir (\dir -> liftE $ applyPatches dir workdir)
+        liftE $ applyAnyPatch patches workdir
 
         pure (workdir, tmpUnpack, tver)
 
@@ -2151,7 +2157,7 @@
       Right GitBranch{..} -> do
         tmpUnpack <- lift mkGhcupTmpDir
         let git args = execLogged "git" ("--no-pager":args) (Just tmpUnpack) "git" Nothing
-        tver <- reThrowAll @_ @'[PatchFailed, ProcessError, NotFoundInPATH] DownloadFailed $ do
+        tver <- reThrowAll @_ @'[PatchFailed, ProcessError, NotFoundInPATH, DigestError, DownloadFailed, GPGError] DownloadFailed $ do
           let rep = fromMaybe "https://gitlab.haskell.org/ghc/ghc.git" repo
           lift $ logInfo $ "Fetching git repo " <> T.pack rep <> " at ref " <> T.pack ref <> " (this may take a while)"
           lEM $ git [ "init" ]
@@ -2171,7 +2177,7 @@
 
           lEM $ git [ "checkout", "FETCH_HEAD" ]
           lEM $ git [ "submodule", "update", "--init", "--depth", "1" ]
-          forM_ patchdir (\dir -> liftE $ applyPatches dir tmpUnpack)
+          liftE $ applyAnyPatch patches tmpUnpack
           lEM $ execWithGhcEnv "python3" ["./boot"] (Just tmpUnpack) "ghc-bootstrap"
           lEM $ execWithGhcEnv "sh" ["./configure"] (Just tmpUnpack) "ghc-bootstrap"
           CapturedProcess {..} <- lift $ makeOut
@@ -2505,6 +2511,7 @@
   execWithGhcEnv :: ( MonadReader env m
                     , HasSettings env
                     , HasDirs env
+                    , HasLog env
                     , MonadIO m
                     , MonadThrow m)
                  => FilePath         -- ^ thing to execute
@@ -2576,7 +2583,7 @@
 
   lift $ logInfo "Upgrading GHCup..."
   let latestVer = fromJust $ fst <$> getLatest dls GHCup
-  (Just ghcupPVPVer) <- pure $ pvpToVersion ghcUpVer
+  (Just ghcupPVPVer) <- pure $ pvpToVersion ghcUpVer ""
   when (not force' && (latestVer <= ghcupPVPVer)) $ throwE NoUpdate
   dli   <- liftE $ getDownloadInfo GHCup latestVer
   tmp   <- lift withGHCupTmpDir
@@ -2845,3 +2852,25 @@
     let p = tmpdir </> f
     logDebug $ "rm -rf " <> T.pack p
     rmPathForcibly p
+
+
+applyAnyPatch :: ( MonadReader env m
+                 , HasDirs env
+                 , HasLog env
+                 , HasSettings env
+                 , MonadUnliftIO m
+                 , MonadCatch m
+                 , MonadResource m
+                 , MonadThrow m
+                 , MonadMask m
+                 , MonadIO m)
+              => Maybe (Either FilePath [URI])
+              -> FilePath
+              -> Excepts '[PatchFailed, DownloadFailed, DigestError, GPGError] m ()
+applyAnyPatch Nothing _                   = pure ()
+applyAnyPatch (Just (Left pdir)) workdir  = liftE $ applyPatches pdir workdir
+applyAnyPatch (Just (Right uris)) workdir = do
+  tmpUnpack <- lift withGHCupTmpDir
+  forM_ uris $ \uri -> do
+    patch <- liftE $ download uri Nothing Nothing tmpUnpack Nothing False
+    liftE $ applyPatch patch workdir
diff --git a/lib/GHCup/Download.hs b/lib/GHCup/Download.hs
--- a/lib/GHCup/Download.hs
+++ b/lib/GHCup/Download.hs
@@ -242,14 +242,18 @@
     e <- liftIO $ doesFileExist json_file
     currentTime <- liftIO getCurrentTime
     Dirs { cacheDir } <- lift getDirs
+    Settings { metaCache } <- lift getSettings
 
        -- for local files, let's short-circuit and ignore access time
     if | scheme == "file" -> liftE $ download uri' Nothing Nothing cacheDir Nothing True
        | e -> do
-          accessTime <- liftIO $ getAccessTime json_file
-
+          accessTime <- fmap utcTimeToPOSIXSeconds $ liftIO $ getAccessTime json_file
+          let sinceLastAccess = utcTimeToPOSIXSeconds currentTime - accessTime
+          let cacheInterval = fromInteger metaCache
+          lift $ logDebug $ "last access was " <> T.pack (show sinceLastAccess) <> " ago, cache interval is " <> T.pack (show cacheInterval)
           -- access time won't work on most linuxes, but we can try regardless
-          if | ((utcTimeToPOSIXSeconds currentTime - utcTimeToPOSIXSeconds accessTime) > 300) ->
+          if | metaCache <= 0 -> dlWithMod currentTime json_file
+             | (sinceLastAccess > cacheInterval) ->
                 -- no access in last 5 minutes, re-check upstream mod time
                 dlWithMod currentTime json_file
              | otherwise -> pure json_file
diff --git a/lib/GHCup/Platform.hs b/lib/GHCup/Platform.hs
--- a/lib/GHCup/Platform.hs
+++ b/lib/GHCup/Platform.hs
@@ -1,3 +1,4 @@
+{-# LANGUAGE CPP                   #-}
 {-# LANGUAGE DataKinds             #-}
 {-# LANGUAGE FlexibleContexts      #-}
 {-# LANGUAGE FlexibleInstances     #-}
@@ -27,6 +28,9 @@
 import           GHCup.Utils.Prelude
 import           GHCup.Utils.String.QQ
 
+#if !MIN_VERSION_base(4,13,0)
+import           Control.Monad.Fail             ( MonadFail )
+#endif
 import           Control.Applicative
 import           Control.Exception.Safe
 import           Control.Monad
diff --git a/lib/GHCup/Types.hs b/lib/GHCup/Types.hs
--- a/lib/GHCup/Types.hs
+++ b/lib/GHCup/Types.hs
@@ -294,6 +294,7 @@
 
 data UserSettings = UserSettings
   { uCache       :: Maybe Bool
+  , uMetaCache   :: Maybe Integer
   , uNoVerify    :: Maybe Bool
   , uVerbose     :: Maybe Bool
   , uKeepDirs    :: Maybe KeepDirs
@@ -306,12 +307,13 @@
   deriving (Show, GHC.Generic)
 
 defaultUserSettings :: UserSettings
-defaultUserSettings = UserSettings Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing
+defaultUserSettings = UserSettings Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing
 
 fromSettings :: Settings -> Maybe KeyBindings -> UserSettings
 fromSettings Settings{..} Nothing =
   UserSettings {
       uCache = Just cache
+    , uMetaCache = Just metaCache
     , uNoVerify = Just noVerify
     , uVerbose = Just verbose
     , uKeepDirs = Just keepDirs
@@ -335,6 +337,7 @@
             }
   in UserSettings {
       uCache = Just cache
+    , uMetaCache = Just metaCache
     , uNoVerify = Just noVerify
     , uVerbose = Just verbose
     , uKeepDirs = Just keepDirs
@@ -410,6 +413,7 @@
 
 data Settings = Settings
   { cache      :: Bool
+  , metaCache  :: Integer
   , noVerify   :: Bool
   , keepDirs   :: KeepDirs
   , downloader :: Downloader
@@ -420,6 +424,12 @@
   , noColor    :: Bool -- this also exists in LoggerConfig
   }
   deriving (Show, GHC.Generic)
+
+defaultMetaCache :: Integer
+defaultMetaCache = 300 -- 5 minutes
+
+defaultSettings :: Settings
+defaultSettings = Settings False defaultMetaCache False Never Curl False GHCupURL False GPGNone False
 
 instance NFData Settings
 
diff --git a/lib/GHCup/Utils.hs b/lib/GHCup/Utils.hs
--- a/lib/GHCup/Utils.hs
+++ b/lib/GHCup/Utils.hs
@@ -59,6 +59,7 @@
 import           Control.Monad.Trans.Resource
                                          hiding ( throwM )
 import           Control.Monad.IO.Unlift        ( MonadUnliftIO( withRunInIO ) )
+import           Data.Bifunctor                 ( first )
 import           Data.ByteString                ( ByteString )
 import           Data.Either
 import           Data.Foldable
@@ -66,7 +67,7 @@
 import           Data.List.NonEmpty             ( NonEmpty( (:|) ))
 import           Data.Maybe
 import           Data.Text                      ( Text )
-import           Data.Versions
+import           Data.Versions         hiding   ( patch )
 import           GHC.IO.Exception
 import           Haskus.Utils.Variant.Excepts
 import           Optics
@@ -110,8 +111,8 @@
 -- >>> import Text.PrettyPrint.HughesPJClass ( prettyShow )
 -- >>> let lc = LoggerConfig { lcPrintDebug = False, consoleOutter = mempty, fileOutter = mempty, fancyColors = False }
 -- >>> dirs' <- getAllDirs
--- >>> let installedVersions = [ ([pver|8.10.7|], Nothing), ([pver|8.10.4|], Nothing), ([pver|8.8.4|], Nothing), ([pver|8.8.3|], Nothing) ]
--- >>> let settings = Settings True False Never Curl False GHCupURL True GPGNone False
+-- >>> let installedVersions = [ ([pver|8.10.7|], "-debug+lol", Nothing), ([pver|8.10.4|], "", Nothing), ([pver|8.8.4|], "", Nothing), ([pver|8.8.3|], "", Nothing) ]
+-- >>> let settings = Settings True 0 False Never Curl False GHCupURL True GPGNone False
 -- >>> let leanAppState = LeanAppState settings dirs' defaultKeyBindings lc
 -- >>> cwd <- getCurrentDirectory
 -- >>> (Right ref) <- pure $ parseURI strictURIParserOptions $ "file://" <> E.encodeUtf8 (T.pack cwd) <> "/data/metadata/" <> (urlBaseName . view pathL' $ ghcupURL)
@@ -631,34 +632,34 @@
   ghcs <- rights <$> getInstalledGHCs
   -- we're permissive here... failed parse just means we have no match anyway
   let ghcs' = catMaybes $ flip fmap ghcs $ \GHCTargetVersion{..} -> do
-        pvp_ <- versionToPVP _tvVersion
-        pure (pvp_, _tvTarget)
+        (pvp_, rest) <- versionToPVP _tvVersion
+        pure (pvp_, rest, _tvTarget)
 
   getGHCForPVP' pvpIn ghcs' mt
 
 -- | Like 'getGHCForPVP', except with explicit input parameter.
 --
--- >>> fmap prettyShow $ getGHCForPVP' [pver|8|] installedVersions Nothing
--- "Just 8.10.7"
+-- >>> getGHCForPVP' [pver|8|] installedVersions Nothing
+-- Just (GHCTargetVersion {_tvTarget = Nothing, _tvVersion = Version {_vEpoch = Nothing, _vChunks = (Digits 8 :| []) :| [Digits 10 :| [],Digits 7 :| []], _vRel = [Str "debug" :| []], _vMeta = Just "lol"}})
 -- >>> fmap prettyShow $ getGHCForPVP' [pver|8.8|] installedVersions Nothing
 -- "Just 8.8.4"
 -- >>> fmap prettyShow $ getGHCForPVP' [pver|8.10.4|] installedVersions Nothing
 -- "Just 8.10.4"
 getGHCForPVP' :: MonadThrow m
              => PVP
-             -> [(PVP, Maybe Text)] -- ^ installed GHCs
+             -> [(PVP, Text, Maybe Text)] -- ^ installed GHCs
              -> Maybe Text          -- ^ the target triple
              -> m (Maybe GHCTargetVersion)
 getGHCForPVP' pvpIn ghcs' mt = do
   let mResult = lastMay
-                  . sortBy (\(x, _) (y, _) -> compare x y)
+                  . sortBy (\(x, _, _) (y, _, _) -> compare x y)
                   . filter
-                      (\(pvp_, target) ->
+                      (\(pvp_, _, target) ->
                         target == mt && matchPVPrefix pvp_ pvpIn
                       )
                   $ ghcs'
-  forM mResult $ \(pvp_, target) -> do
-    ver' <- pvpToVersion pvp_
+  forM mResult $ \(pvp_, rest, target) -> do
+    ver' <- pvpToVersion pvp_ rest
     pure (GHCTargetVersion target ver')
 
 
@@ -679,7 +680,7 @@
 getLatestToolFor tool pvpIn dls = do
   let ls = fromMaybe [] $ preview (ix tool % to Map.toDescList) dls
   let ps = catMaybes $ fmap (\(v, vi) -> (,vi) <$> versionToPVP v) ls
-  pure . headMay . filter (\(v, _) -> matchPVPrefix pvpIn v) $ ps
+  pure . fmap (first fst) . headMay . filter (\((v, _), _) -> matchPVPrefix pvpIn v) $ ps
 
 
 
@@ -855,6 +856,7 @@
         , MonadIO m
         , MonadReader env m
         , HasDirs env
+        , HasLog env
         , HasSettings env
         )
      => [String]
@@ -890,15 +892,22 @@
                      execBlank
                      ([s|.+\.(patch|diff)$|] :: ByteString)
       )
-  forM_ (sort patches) $ \patch' -> do
-    lift $ logInfo $ "Applying patch " <> T.pack patch'
-    fmap (either (const Nothing) Just)
-         (exec
-           "patch"
-           ["-p1", "-i", patch']
-           (Just ddir)
-           Nothing)
-      !? PatchFailed
+  forM_ (sort patches) $ \patch' -> applyPatch patch' ddir
+
+
+applyPatch :: (MonadReader env m, HasDirs env, HasLog env, MonadIO m)
+           => FilePath   -- ^ Patch
+           -> FilePath   -- ^ dir to apply patches in
+           -> Excepts '[PatchFailed] m ()
+applyPatch patch ddir = do
+  lift $ logInfo $ "Applying patch " <> T.pack patch
+  fmap (either (const Nothing) Just)
+       (exec
+         "patch"
+         ["-p1", "-s", "-f", "-i", patch]
+         (Just ddir)
+         Nothing)
+    !? PatchFailed
 
 
 -- | https://gitlab.haskell.org/ghc/ghc/-/issues/17353
diff --git a/lib/GHCup/Utils/File/Posix.hs b/lib/GHCup/Utils/File/Posix.hs
--- a/lib/GHCup/Utils/File/Posix.hs
+++ b/lib/GHCup/Utils/File/Posix.hs
@@ -73,6 +73,7 @@
 
 execLogged :: ( MonadReader env m
               , HasSettings env
+              , HasLog env
               , HasDirs env
               , MonadIO m
               , MonadThrow m)
@@ -85,6 +86,7 @@
 execLogged exe args chdir lfile env = do
   Settings {..} <- getSettings
   Dirs {..} <- getDirs
+  logDebug $ T.pack $ "Running " <> exe <> " with arguments " <> show args
   let logfile = logsDir </> lfile <> ".log"
   liftIO $ bracket (openFd logfile WriteOnly (Just newFilePerms) defaultFileFlags{ append = True })
                    closeFd
diff --git a/lib/GHCup/Utils/File/Windows.hs b/lib/GHCup/Utils/File/Windows.hs
--- a/lib/GHCup/Utils/File/Windows.hs
+++ b/lib/GHCup/Utils/File/Windows.hs
@@ -18,6 +18,7 @@
 import {-# SOURCE #-} GHCup.Utils ( getLinkTarget, pathIsLink )
 import           GHCup.Utils.Dirs
 import           GHCup.Utils.File.Common
+import           GHCup.Utils.Logger
 import           GHCup.Types
 import           GHCup.Types.Optics
 
@@ -40,6 +41,7 @@
 import qualified Data.ByteString               as BS
 import qualified Data.ByteString.Lazy          as BL
 import qualified Data.Map.Strict               as Map
+import qualified Data.Text                     as T
 
 
 
@@ -149,6 +151,7 @@
 
 execLogged :: ( MonadReader env m
               , HasDirs env
+              , HasLog env
               , HasSettings env
               , MonadIO m
               , MonadThrow m)
@@ -160,6 +163,7 @@
            -> m (Either ProcessError ())
 execLogged exe args chdir lfile env = do
   Dirs {..} <- getDirs
+  logDebug $ T.pack $ "Running " <> exe <> " with arguments " <> show args
   let stdoutLogfile = logsDir </> lfile <> ".stdout.log"
       stderrLogfile = logsDir </> lfile <> ".stderr.log"
   cp <- createProcessWithMingwPath ((proc exe args)
diff --git a/lib/GHCup/Utils/Prelude.hs b/lib/GHCup/Utils/Prelude.hs
--- a/lib/GHCup/Utils/Prelude.hs
+++ b/lib/GHCup/Utils/Prelude.hs
@@ -44,7 +44,7 @@
 import           Control.Monad.Reader
 import           Data.Bifunctor
 import           Data.ByteString                ( ByteString )
-import           Data.List                      ( nub, intercalate, stripPrefix, isPrefixOf, dropWhileEnd )
+import           Data.List                      ( nub, intercalate, stripPrefix, isPrefixOf, dropWhileEnd, intersperse )
 import           Data.Maybe
 import           Data.Foldable
 import           Data.List.NonEmpty             ( NonEmpty( (:|) ))
@@ -313,17 +313,45 @@
   maybe str' T.unpack . T.stripPrefix (T.pack "_") . T.pack $ str'
 
 
-pvpToVersion :: MonadThrow m => PVP -> m Version
-pvpToVersion =
-  either (\_ -> throwM $ ParseError "Couldn't convert PVP to Version") pure . version . prettyPVP
+pvpToVersion :: MonadThrow m => PVP -> Text -> m Version
+pvpToVersion pvp_ rest =
+  either (\_ -> throwM $ ParseError "Couldn't convert PVP to Version") pure . version . (<> rest) . prettyPVP $ pvp_
 
-versionToPVP :: MonadThrow m => Version -> m PVP
-versionToPVP v = either (\_ -> alternative v) pure . pvp . prettyVer $ v
+-- | Convert a version to a PVP and unparsable rest.
+--
+-- -- prop> \v -> let (Just (pvp', r)) = versionToPVP v in pvpToVersion pvp' r === Just v
+versionToPVP :: MonadThrow m => Version -> m (PVP, Text)
+versionToPVP (Version (Just _) _ _ _) = throwM $ ParseError "Unexpected epoch"
+versionToPVP v = either (\_ -> (, rest v) <$> alternative v) (pure . (, mempty)) . pvp . prettyVer $ v
  where
   alternative :: MonadThrow m => Version -> m PVP
   alternative v' = case NE.takeWhile isDigit (_vChunks v') of
     [] -> throwM $ ParseError "Couldn't convert Version to PVP"
     xs -> pure $ pvpFromList (unsafeDigit <$> xs)
+
+  rest :: Version -> Text
+  rest (Version _ cs pr me) =
+    let chunks = NE.dropWhile isDigit cs
+        ver = intersperse (T.pack ".") . chunksAsT $ chunks
+        me' = maybe [] (\m -> [T.pack "+",m]) me
+        pr' = foldable [] (T.pack "-" :) $ intersperse (T.pack ".") (chunksAsT pr)
+        prefix = case (ver, pr', me') of
+                   (_:_, _, _) -> T.pack "."
+                   _           -> T.pack ""
+    in prefix <> mconcat (ver <> pr' <> me')
+   where
+    chunksAsT :: Functor t => t VChunk -> t Text
+    chunksAsT = fmap (foldMap f)
+      where
+        f :: VUnit -> Text
+        f (Digits i) = T.pack $ show i
+        f (Str s)    = s
+
+    foldable :: Foldable f => f b -> (f a -> f b) -> f a -> f b
+    foldable d g f | null f    = d
+                   | otherwise = g f
+
+
 
   isDigit :: VChunk -> Bool
   isDigit (Digits _ :| []) = True
diff --git a/lib/GHCup/Utils/Version/QQ.hs b/lib/GHCup/Utils/Version/QQ.hs
--- a/lib/GHCup/Utils/Version/QQ.hs
+++ b/lib/GHCup/Utils/Version/QQ.hs
@@ -53,6 +53,9 @@
 
 #if !MIN_VERSION_base(4,13,0)
 deriving instance Lift (NonEmpty Word)
+deriving instance Lift (NonEmpty VChunk)
+deriving instance Lift (NonEmpty MChunk)
+deriving instance Lift (NonEmpty VUnit)
 #endif
 
 qq :: (Text -> Q Exp) -> QuasiQuoter
diff --git a/test/GHCup/Types/JSONSpec.hs b/test/GHCup/Types/JSONSpec.hs
--- a/test/GHCup/Types/JSONSpec.hs
+++ b/test/GHCup/Types/JSONSpec.hs
@@ -3,7 +3,7 @@
 module GHCup.Types.JSONSpec where
 
 import           GHCup.ArbitraryTypes ()
-import           GHCup.Types
+import           GHCup.Types hiding ( defaultSettings )
 import           GHCup.Types.JSON ()
 
 import           Test.Aeson.GenericSpecs
