diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,18 @@
 # Revision history for ghcup
 
+## 0.1.17.5 -- 2022-02-26
+
+* Implement `ghcup run` subcommand wrt [#137](https://gitlab.haskell.org/haskell/ghcup-hs/-/issues/137)
+* Support installation of dynamic HLS bindists wrt [HLS #2675](https://github.com/haskell/haskell-language-server/pull/2675) and [#237](https://gitlab.haskell.org/haskell/ghcup-hs/-/merge_requests/237)
+* Fix XDG support when `~/.local/bin` is a symlink wrt [#311](https://gitlab.haskell.org/haskell/ghcup-hs/-/merge_requests/311)
+* Add support for quilt-style patches wrt [#230](https://gitlab.haskell.org/haskell/ghcup-hs/-/merge_requests/230), by James Hobson
+* Fix redundant upgrade warnings in `ghcup upgrade`
+* Fix `ghcup whereis ghc` for non-standard versions wrt [#289](https://gitlab.haskell.org/haskell/ghcup-hs/-/issues/289)
+* Don't print logs to stdout, but stderr
+* Allow unpacking legacy lzma archives wrt [#307](https://gitlab.haskell.org/haskell/ghcup-hs/-/issues/307)
+* Allow to disable self-upgrade functionality wrt [#305](https://gitlab.haskell.org/haskell/ghcup-hs/-/issues/305)
+* Fix `ghcup install ghc --set` when ghc is already installed wrt [#291](https://gitlab.haskell.org/haskell/ghcup-hs/-/issues/291)
+
 ## 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)
@@ -7,6 +20,7 @@
 * 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
+* avoid redundant update warnings wrt [#283](https://gitlab.haskell.org/haskell/ghcup-hs/-/issues/283)
 
 ## 0.1.17.3 -- 2021-10-27
 
diff --git a/app/ghcup/BrickMain.hs b/app/ghcup/BrickMain.hs
--- a/app/ghcup/BrickMain.hs
+++ b/app/ghcup/BrickMain.hs
@@ -493,9 +493,9 @@
 
   run (do
       case lTool of
-        GHC   -> liftE $ setGHC (GHCTargetVersion lCross lVer) SetGHCOnly $> ()
+        GHC   -> liftE $ setGHC (GHCTargetVersion lCross lVer) SetGHCOnly Nothing $> ()
         Cabal -> liftE $ setCabal lVer $> ()
-        HLS   -> liftE $ setHLS lVer $> ()
+        HLS   -> liftE $ setHLS lVer SetHLSOnly Nothing $> ()
         Stack -> liftE $ setStack lVer $> ()
         GHCup -> pure ()
     )
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
@@ -15,13 +15,16 @@
   , module GHCup.OptParse.Config
   , module GHCup.OptParse.Whereis
   , module GHCup.OptParse.List
+#ifndef DISABLE_UPGRADE
   , module GHCup.OptParse.Upgrade
+#endif
   , module GHCup.OptParse.ChangeLog
   , module GHCup.OptParse.Prefetch
   , module GHCup.OptParse.GC
   , module GHCup.OptParse.DInfo
   , module GHCup.OptParse.Nuke
   , module GHCup.OptParse.ToolRequirements
+  , module GHCup.OptParse.Run
   , module GHCup.OptParse
 ) where
 
@@ -31,11 +34,14 @@
 import           GHCup.OptParse.Set
 import           GHCup.OptParse.UnSet
 import           GHCup.OptParse.Rm
+import           GHCup.OptParse.Run
 import           GHCup.OptParse.Compile
 import           GHCup.OptParse.Config
 import           GHCup.OptParse.Whereis
 import           GHCup.OptParse.List
+#ifndef DISABLE_UPGRADE
 import           GHCup.OptParse.Upgrade
+#endif
 import           GHCup.OptParse.ChangeLog
 import           GHCup.OptParse.Prefetch
 import           GHCup.OptParse.GC
@@ -89,7 +95,9 @@
   | Compile CompileCommand
   | Config ConfigCommand
   | Whereis WhereisOptions WhereisCommand
+#ifndef DISABLE_UPGRADE
   | Upgrade UpgradeOpts Bool
+#endif
   | ToolRequirements
   | ChangeLog ChangeLogOptions
   | Nuke
@@ -98,6 +106,7 @@
 #endif
   | Prefetch PrefetchCommand
   | GC GCOptions
+  | Run RunOptions
 
 
 
@@ -208,6 +217,7 @@
            (info (List <$> listOpts <**> helper)
                  (progDesc "Show available GHCs and other tools")
            )
+#ifndef DISABLE_UPGRADE
       <> command
            "upgrade"
            (info
@@ -218,6 +228,7 @@
              )
              (progDesc "Upgrade ghcup")
            )
+#endif
       <> command
            "compile"
            (   Compile
@@ -255,6 +266,16 @@
              (progDesc "Garbage collection"
              <> footerDoc ( Just $ text gcFooter ))
            )
+      <> command
+              "run"
+               (Run
+               <$>
+                 info
+                   (runOpts <**> helper)
+                   (progDesc "Run a command with the given tool in PATH"
+                   <> footerDoc ( Just $ text runFooter )
+                   )
+               )
       <> commandGroup "Main commands:"
       )
     <|> subparser
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,18 +89,6 @@
   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)
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
@@ -212,7 +212,7 @@
             (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)"
+                "Absolute path to patch directory (applies all .patch and .diff files in order using -p1. This order is determined by a quilt series file if it exists, or the patches are lexicographically ordered)"
               )
             )
           )
@@ -340,7 +340,12 @@
             )
           )
         )
-    <*> some (toolVersionOption Nothing (Just GHC))
+    <*> some (
+          option (eitherReader toolVersionEither)
+            (  long "ghc" <> metavar "GHC_VERSION|TAG" <> help "For which GHC version to compile for (can be specified multiple times)"
+            <> completer (tagCompleter GHC [])
+            <> completer (versionCompleter Nothing GHC))
+        )
     <*> many (argument str (metavar "CABAL_ARGS" <> help "Additional arguments to cabal install, prefix with '-- ' (longopts)"))
 
 
@@ -461,7 +466,7 @@
         GHCupInfo { _ghcupDownloads = dls } <- lift getGHCupInfo
         let vi = getVersionInfo targetVer HLS dls
         when setCompile $ void $ liftE $
-          setHLS targetVer
+          setHLS targetVer SetHLSOnly Nothing
         pure (vi, targetVer)
         )
         >>= \case
@@ -512,7 +517,7 @@
         GHCupInfo { _ghcupDownloads = dls } <- lift getGHCupInfo
         let vi = getVersionInfo (_tvVersion targetVer) GHC dls
         when setCompile $ void $ liftE $
-          setGHC targetVer SetGHCOnly
+          setGHC targetVer SetGHCOnly Nothing
         pure (vi, targetVer)
         )
         >>= \case
diff --git a/app/ghcup/GHCup/OptParse/GC.hs b/app/ghcup/GHCup/OptParse/GC.hs
--- a/app/ghcup/GHCup/OptParse/GC.hs
+++ b/app/ghcup/GHCup/OptParse/GC.hs
@@ -132,7 +132,7 @@
   when gcOldGHC rmOldGHC
   lift $ when gcProfilingLibs rmProfilingLibs
   lift $ when gcShareDir rmShareDir
-  lift $ when gcHLSNoGHC rmHLSNoGHC
+  liftE $ when gcHLSNoGHC rmHLSNoGHC
   lift $ when gcCache rmCache
   lift $ when gcTmp rmTmp
    ) >>= \case
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
@@ -17,7 +17,6 @@
 import           GHCup
 import           GHCup.Errors
 import           GHCup.Types
-import           GHCup.Utils.File
 import           GHCup.Utils.Logger
 import           GHCup.Utils.String.QQ
 
@@ -268,7 +267,65 @@
     @InstallEffects
 
 
+type InstallGHCEffects = '[ TagNotFound
+                          , NextVerNotFound
+                          , NoToolVersionSet
+                          , BuildFailed
+                          , DirNotEmpty
+                          , AlreadyInstalled
 
+                          , (AlreadyInstalled, NotInstalled)
+                          , (UnknownArchive, NotInstalled)
+                          , (ArchiveResult, NotInstalled)
+                          , (FileDoesNotExistError, NotInstalled)
+                          , (CopyError, NotInstalled)
+                          , (NotInstalled, NotInstalled)
+                          , (DirNotEmpty, NotInstalled)
+                          , (NoDownload, NotInstalled)
+                          , (BuildFailed, NotInstalled)
+                          , (TagNotFound, NotInstalled)
+                          , (DigestError, NotInstalled)
+                          , (GPGError, NotInstalled)
+                          , (DownloadFailed, NotInstalled)
+                          , (TarDirDoesNotExist, NotInstalled)
+                          , (NextVerNotFound, NotInstalled)
+                          , (NoToolVersionSet, NotInstalled)
+                          , (FileAlreadyExistsError, NotInstalled)
+                          , (ProcessError, NotInstalled)
+
+                          , (AlreadyInstalled, ())
+                          , (UnknownArchive, ())
+                          , (ArchiveResult, ())
+                          , (FileDoesNotExistError, ())
+                          , (CopyError, ())
+                          , (NotInstalled, ())
+                          , (DirNotEmpty, ())
+                          , (NoDownload, ())
+                          , (BuildFailed, ())
+                          , (TagNotFound, ())
+                          , (DigestError, ())
+                          , (GPGError, ())
+                          , (DownloadFailed, ())
+                          , (TarDirDoesNotExist, ())
+                          , (NextVerNotFound, ())
+                          , (NoToolVersionSet, ())
+                          , (FileAlreadyExistsError, ())
+                          , (ProcessError, ())
+
+                          , ((), NotInstalled)
+                          ]
+
+runInstGHC :: AppState
+           -> Maybe PlatformRequest
+           -> Excepts InstallGHCEffects (ResourceT (ReaderT AppState IO)) a
+           -> IO (VEither InstallGHCEffects a)
+runInstGHC appstate' mInstPlatform =
+  flip runReaderT (maybe appstate' (\x -> appstate'{ pfreq = x } :: AppState) mInstPlatform)
+  . runResourceT
+  . runE
+    @InstallGHCEffects
+
+
     -------------------
     --[ Entrypoints ]--
     -------------------
@@ -288,23 +345,25 @@
   installGHC InstallOptions{..} = do
     s'@AppState{ dirs = Dirs{ .. } } <- liftIO getAppState'
     (case instBindist of
-       Nothing -> runInstTool s' instPlatform $ do
+       Nothing -> runInstGHC s' instPlatform $ do
          (v, vi) <- liftE $ fromVersion instVer GHC
-         liftE $ installGHCBin
-                   (_tvVersion v)
-                   isolateDir
-                   forceInstall
-         when instSet $ void $ liftE $ setGHC v SetGHCOnly
-         pure vi
-       Just uri -> do
-         runInstTool s'{ settings = settings {noVerify = True}} instPlatform $ do
-           (v, vi) <- liftE $ fromVersion instVer GHC
-           liftE $ installGHCBindist
-                     (DownloadInfo uri (Just $ RegexDir "ghc-.*") "")
+         void $ liftE $ sequenceE (installGHCBin
                      (_tvVersion v)
                      isolateDir
                      forceInstall
-           when instSet $ void $ liftE $ setGHC v SetGHCOnly
+                   )
+                   $ when instSet $ void $ setGHC v SetGHCOnly Nothing
+         pure vi
+       Just uri -> do
+         runInstGHC s'{ settings = settings {noVerify = True}} instPlatform $ do
+           (v, vi) <- liftE $ fromVersion instVer GHC
+           void $ liftE $ sequenceE (installGHCBindist
+                       (DownloadInfo uri (Just $ RegexDir "ghc-.*") "")
+                       (_tvVersion v)
+                       isolateDir
+                       forceInstall
+                     )
+                     $ when instSet $ void $ setGHC v SetGHCOnly Nothing
            pure vi
       )
         >>= \case
@@ -313,14 +372,25 @@
                 forM_ (_viPostInstall =<< vi) $ \msg ->
                   runLogger $ logInfo msg
                 pure ExitSuccess
+
+              VLeft (V (AlreadyInstalled _ v, ())) -> do
+                runLogger $ logWarn $
+                  "GHC ver " <> prettyVer v <> " already installed; if you really want to reinstall it, you may want to run 'ghcup install ghc --force " <> prettyVer v <> "'"
+                pure ExitSuccess
               VLeft (V (AlreadyInstalled _ v)) -> do
                 runLogger $ logWarn $
                   "GHC ver " <> prettyVer v <> " already installed; if you really want to reinstall it, you may want to run 'ghcup install ghc --force " <> prettyVer v <> "'"
                 pure ExitSuccess
+
               VLeft (V (DirNotEmpty fp)) -> do
                 runLogger $ logWarn $
                   "Install directory " <> T.pack fp <> " is not empty. Use 'ghcup install ghc --isolate " <> T.pack fp <> " --force ..." <> "' to install regardless."
                 pure $ ExitFailure 3
+              VLeft (V (DirNotEmpty fp, ())) -> do
+                runLogger $ logWarn $
+                  "Install directory " <> T.pack fp <> " is not empty. Use 'ghcup install ghc --isolate " <> T.pack fp <> " --force ..." <> "' to install regardless."
+                pure $ ExitFailure 3
+
               VLeft err@(V (BuildFailed tmpdir _)) -> do
                 case keepDirs settings of
                   Never -> runLogger (logError $ T.pack $ prettyShow err)
@@ -328,6 +398,14 @@
                     "Check the logs at " <> T.pack logsDir <> " and the build directory " <> T.pack tmpdir <> " for more clues." <> "\n" <>
                     "Make sure to clean up " <> T.pack tmpdir <> " afterwards.")
                 pure $ ExitFailure 3
+              VLeft err@(V (BuildFailed tmpdir _, ())) -> do
+                case keepDirs settings of
+                  Never -> runLogger (logError $ T.pack $ prettyShow err)
+                  _ -> runLogger (logError $ T.pack (prettyShow err) <> "\n" <>
+                    "Check the logs at " <> T.pack logsDir <> " and the build directory " <> T.pack tmpdir <> " for more clues." <> "\n" <>
+                    "Make sure to clean up " <> T.pack tmpdir <> " afterwards.")
+                pure $ ExitFailure 3
+
               VLeft e -> do
                 runLogger $ do
                   logError $ T.pack $ prettyShow e
@@ -390,8 +468,9 @@
        Just uri -> do
          runInstTool s'{ settings = settings { noVerify = True}} instPlatform $ do
            (v, vi) <- liftE $ fromVersion instVer HLS
+           -- TODO: support legacy
            liftE $ installHLSBindist
-                     (DownloadInfo uri Nothing "")
+                     (DownloadInfo uri (Just $ RegexDir "haskell-language-server-*") "")
                      (_tvVersion v)
                      isolateDir
                      forceInstall
diff --git a/app/ghcup/GHCup/OptParse/Run.hs b/app/ghcup/GHCup/OptParse/Run.hs
new file mode 100644
--- /dev/null
+++ b/app/ghcup/GHCup/OptParse/Run.hs
@@ -0,0 +1,348 @@
+{-# LANGUAGE CPP               #-}
+{-# LANGUAGE QuasiQuotes       #-}
+{-# LANGUAGE TypeApplications  #-}
+{-# LANGUAGE DataKinds         #-}
+{-# LANGUAGE RankNTypes        #-}
+{-# LANGUAGE FlexibleContexts  #-}
+{-# LANGUAGE TypeFamilies      #-}
+module GHCup.OptParse.Run where
+
+
+import           GHCup
+import           GHCup.Utils
+import           GHCup.Utils.Prelude
+import           GHCup.Utils.File
+import           GHCup.OptParse.Common
+import           GHCup.Errors
+import           GHCup.Types
+import           GHCup.Types.Optics             ( getDirs )
+import           GHCup.Utils.Logger
+import           GHCup.Utils.String.QQ
+
+import           Control.Exception.Safe         ( MonadMask, MonadCatch )
+#if !MIN_VERSION_base(4,13,0)
+import           Control.Monad.Fail             ( MonadFail )
+#endif
+import           Codec.Archive
+import           Control.Monad.Reader
+import           Control.Monad.Trans.Resource
+import           Data.Functor
+import           Data.Maybe (isNothing)
+import           Data.List                      ( intercalate )
+import           Haskus.Utils.Variant.Excepts
+import           Options.Applicative     hiding ( style )
+import           Prelude                 hiding ( appendFile )
+import           System.Directory
+import           System.FilePath
+import           System.Environment
+import           System.IO.Temp
+import           System.Exit
+import           Text.PrettyPrint.HughesPJClass ( prettyShow )
+
+import qualified Data.Map.Strict               as Map
+import qualified Data.Text                     as T
+#ifndef IS_WINDOWS
+import qualified System.Posix.Process          as SPP
+#endif
+
+
+
+
+
+    ---------------
+    --[ Options ]--
+    ---------------
+
+
+data RunOptions = RunOptions
+  { runAppendPATH :: Bool
+  , runInstTool'  :: Bool
+  , runGHCVer     :: Maybe ToolVersion
+  , runCabalVer   :: Maybe ToolVersion
+  , runHLSVer     :: Maybe ToolVersion
+  , runStackVer   :: Maybe ToolVersion
+  , runBinDir     :: Maybe FilePath
+  , runCOMMAND    :: [String]
+  }
+
+
+
+    ---------------
+    --[ Parsers ]--
+    ---------------
+
+          
+runOpts :: Parser RunOptions
+runOpts =
+  RunOptions
+    <$> switch
+          (short 'a' <> long "append" <> help "Append bin/ dir to PATH instead of prepending (this means that e.g. a system installation may take precedence)")
+    <*> switch
+          (short 'i' <> long "install" <> help "Install the tool, if missing")
+    <*> optional
+          (option
+            (eitherReader toolVersionEither)
+            (metavar "GHC_VERSION" <> long "ghc" <> help "The ghc version")
+          )
+    <*> optional
+          (option
+            (eitherReader toolVersionEither)
+            (metavar "CABAL_VERSION" <> long "cabal" <> help "The cabal version")
+          )
+    <*> optional
+          (option
+            (eitherReader toolVersionEither)
+            (metavar "HLS_VERSION" <> long "hls" <> help "The HLS version")
+          )
+    <*> optional
+          (option
+            (eitherReader toolVersionEither)
+            (metavar "STACK_VERSION" <> long "stack" <> help "The stack version")
+          )
+    <*> optional
+          (option
+           (eitherReader isolateParser)
+           (  short 'b'
+           <> long "bindir"
+           <> metavar "DIR"
+           <> help "directory where to create the tool symlinks (default: newly created system temp dir)"
+           )
+          )
+    <*> many (argument str (metavar "COMMAND" <> help "The command to run, with arguments (use longopts --). If omitted, just prints the created bin/ dir to stdout and exits."))
+          
+
+
+
+    --------------
+    --[ Footer ]--
+    --------------
+
+
+runFooter :: String
+runFooter = [s|Discussion:
+  Adds the given tools to a dedicated bin/ directory and adds them to PATH, exposing
+  the relevant binaries, then executes a command.
+
+Examples:
+  # run VSCode with all latest toolchain exposed, installing missing versions if necessary
+  ghcup run --ghc latest --cabal latest --hls latest --stack latest --install -- code Setup.hs
+
+  # create a custom toolchain bin/ dir with GHC and cabal that can be manually added to PATH
+  ghcup run --ghc 8.10.7 --cabal 3.2.0.0 --bindir $HOME/toolchain/bin
+
+  # run a specific ghc version
+  ghcup run --ghc 8.10.7 -- ghc --version|]
+
+
+
+
+    ---------------------------
+    --[ Effect interpreters ]--
+    ---------------------------
+
+
+type RunEffects = '[ AlreadyInstalled
+                   , UnknownArchive
+                   , ArchiveResult
+                   , FileDoesNotExistError
+                   , CopyError
+                   , NotInstalled
+                   , DirNotEmpty
+                   , NoDownload
+                   , NotInstalled
+                   , BuildFailed
+                   , TagNotFound
+                   , DigestError
+                   , GPGError
+                   , DownloadFailed
+                   , TarDirDoesNotExist
+                   , NextVerNotFound
+                   , NoToolVersionSet
+                   , FileAlreadyExistsError
+                   , ProcessError
+                   ]
+
+runLeanRUN :: (MonadUnliftIO m, MonadIO m)
+           => LeanAppState
+           -> Excepts RunEffects (ReaderT LeanAppState m) a
+           -> m (VEither RunEffects a)
+runLeanRUN leanAppstate =
+    -- Don't use runLeanAppState here, which is disabled on windows.
+    -- This is the only command on all platforms that doesn't need full appstate.
+    flip runReaderT leanAppstate
+    . runE
+      @RunEffects
+
+runRUN :: MonadUnliftIO m
+      => (ReaderT AppState m (VEither RunEffects a) -> m (VEither RunEffects a))
+      -> Excepts RunEffects (ResourceT (ReaderT AppState m)) a
+      -> m (VEither RunEffects a)
+runRUN runAppState =
+  runAppState
+    . runResourceT
+    . runE
+      @RunEffects
+
+
+
+    ------------------
+    --[ Entrypoint ]--
+    ------------------
+
+
+
+run :: forall m. 
+       ( MonadFail m
+       , MonadMask m
+       , MonadCatch m
+       , MonadIO m
+       , MonadUnliftIO m
+       )
+   => RunOptions
+   -> (forall a. ReaderT AppState m (VEither RunEffects a) -> m (VEither RunEffects a))
+   -> LeanAppState
+   -> (ReaderT LeanAppState m () -> m ())
+   -> m ExitCode
+run RunOptions{..} runAppState leanAppstate runLogger = do
+   tmp <- case runBinDir of
+     Just bdir -> do
+       liftIO $ createDirRecursive' bdir
+       liftIO $ canonicalizePath bdir
+     Nothing -> liftIO (getTemporaryDirectory >>= \tmp -> createTempDirectory tmp "ghcup")
+   r <- do
+     addToolsToDir tmp
+   case r of
+     VRight _ -> do
+       case runCOMMAND of
+         [] -> do
+           liftIO $ putStr tmp
+           pure ExitSuccess
+         (cmd:args) -> do
+           newEnv <- liftIO $ addToPath tmp
+#ifndef IS_WINDOWS
+           void $ liftIO $ SPP.executeFile cmd True args (Just newEnv)
+           pure ExitSuccess
+#else
+           r' <- runLeanRUN leanAppstate $ liftE $ lEM @_ @'[ProcessError] $ exec cmd args Nothing (Just newEnv)
+           case r' of
+             VRight _ -> pure ExitSuccess
+             VLeft e -> do
+               runLogger $ logError $ T.pack $ prettyShow e
+               pure $ ExitFailure 28
+#endif
+     VLeft e -> do
+       runLogger $ logError $ T.pack $ prettyShow e
+       pure $ ExitFailure 27
+  where
+   isToolTag :: ToolVersion -> Bool
+   isToolTag (ToolTag _) = True
+   isToolTag _           = False
+
+   -- TODO: doesn't work for cross
+   addToolsToDir tmp
+     | or (fmap (maybe False isToolTag) [runGHCVer, runCabalVer, runHLSVer, runStackVer]) || runInstTool' = runRUN runAppState $ do
+         forM_ runGHCVer $ \ver -> do
+           (v, _) <- liftE $ fromVersion (Just ver) GHC
+           installTool GHC v
+           setTool GHC v tmp
+         forM_ runCabalVer $ \ver -> do
+           (v, _) <- liftE $ fromVersion (Just ver) Cabal
+           installTool Cabal v
+           setTool Cabal v tmp
+         forM_ runHLSVer $ \ver -> do
+           (v, _) <- liftE $ fromVersion (Just ver) HLS
+           installTool HLS v
+           setTool HLS v tmp
+         forM_ runStackVer $ \ver -> do
+           (v, _) <- liftE $ fromVersion (Just ver) Stack
+           installTool Stack v
+           setTool Stack v tmp
+     | otherwise = runLeanRUN leanAppstate $ do
+         case runGHCVer of
+            Just (ToolVersion v) ->
+              setTool GHC v tmp
+            Nothing -> pure ()
+            _ -> fail "Internal error"
+         case runCabalVer of
+            Just (ToolVersion v) ->
+              setTool Cabal v tmp
+            Nothing -> pure ()
+            _ -> fail "Internal error"
+         case runHLSVer of
+            Just (ToolVersion v) ->
+              setTool HLS v tmp
+            Nothing -> pure ()
+            _ -> fail "Internal error"
+         case runStackVer of
+            Just (ToolVersion v) ->
+              setTool Stack v tmp
+            Nothing -> pure ()
+            _ -> fail "Internal error"
+
+   installTool tool v = do
+      isInstalled <- checkIfToolInstalled' tool v
+      case tool of
+        GHC -> do
+          unless isInstalled $ when (runInstTool' && isNothing (_tvTarget v)) $ void $ liftE $ installGHCBin
+            (_tvVersion v)
+            Nothing
+            False
+        Cabal -> do
+          unless isInstalled $ when runInstTool' $ void $ liftE $ installCabalBin
+            (_tvVersion v)
+            Nothing
+            False
+        Stack -> do
+          unless isInstalled $ when runInstTool' $ void $ liftE $ installStackBin
+            (_tvVersion v)
+            Nothing
+            False
+        HLS -> do
+          unless isInstalled $ when runInstTool' $ void $ liftE $ installHLSBin
+            (_tvVersion v)
+            Nothing
+            False
+        GHCup -> pure ()
+
+   setTool tool v tmp =
+      case tool of
+        GHC -> do
+          void $ liftE $ setGHC v SetGHC_XYZ (Just tmp)
+          void $ liftE $ setGHC v SetGHCOnly (Just tmp)
+        Cabal -> do
+          bin  <- liftE $ whereIsTool Cabal v
+          cbin <- liftIO $ canonicalizePath bin
+          lift $ createLink (relativeSymlink tmp cbin) (tmp </> ("cabal" <.> exeExt))
+        Stack -> do
+          bin  <- liftE $ whereIsTool Stack v
+          cbin <- liftIO $ canonicalizePath bin
+          lift $ createLink (relativeSymlink tmp cbin) (tmp </> ("stack" <.> exeExt))
+        HLS -> do
+          Dirs {..}  <- getDirs
+          let v' = _tvVersion v
+          legacy <- isLegacyHLS v'
+          if legacy
+          then do
+            -- TODO: factor this out
+            (Just hlsWrapper) <- hlsWrapperBinary v'
+            cw <- liftIO $ canonicalizePath (binDir </> hlsWrapper)
+            lift $ createLink (relativeSymlink tmp cw) (tmp </> takeFileName cw)
+            hlsBins <- hlsServerBinaries v' Nothing >>= liftIO . traverse (canonicalizePath . (binDir </>))
+            forM_ hlsBins $ \bin ->
+              lift $ createLink (relativeSymlink tmp bin) (tmp </> takeFileName bin)
+            liftE $ setHLS (_tvVersion v) SetHLSOnly (Just tmp)
+          else do
+            liftE $ setHLS (_tvVersion v) SetHLS_XYZ (Just tmp)
+            liftE $ setHLS (_tvVersion v) SetHLSOnly (Just tmp)
+        GHCup -> pure ()
+       
+   addToPath path = do
+    cEnv <- Map.fromList <$> getEnvironment
+    let paths          = ["PATH", "Path"]
+        curPaths       = (\x -> maybe [] splitSearchPath (Map.lookup x cEnv)) =<< paths
+        newPath        = intercalate [searchPathSeparator] (if runAppendPATH then (curPaths ++ [path]) else (path : curPaths))
+        envWithoutPath = foldr (\x y -> Map.delete x y) cEnv paths
+        pathVar        = if isWindows then "Path" else "PATH"
+        envWithNewPath = Map.toList $ Map.insert pathVar newPath envWithoutPath
+    liftIO $ setEnv pathVar newPath
+    return envWithNewPath
diff --git a/app/ghcup/GHCup/OptParse/Set.hs b/app/ghcup/GHCup/OptParse/Set.hs
--- a/app/ghcup/GHCup/OptParse/Set.hs
+++ b/app/ghcup/GHCup/OptParse/Set.hs
@@ -271,10 +271,10 @@
           -> m ExitCode
   setGHC' SetOptions{ sToolVer } =
     case sToolVer of
-      (SetToolVersion v) -> runSetGHC runLeanAppState (liftE $ setGHC v SetGHCOnly >> pure v)
+      (SetToolVersion v) -> runSetGHC runLeanAppState (liftE $ setGHC v SetGHCOnly Nothing >> pure v)
       _ -> runSetGHC runAppState (do
           v <- liftE $ fst <$> fromVersion' sToolVer GHC
-          liftE $ setGHC v SetGHCOnly
+          liftE $ setGHC v SetGHCOnly Nothing
         )
       >>= \case
             VRight GHCTargetVersion{..} -> do
@@ -311,10 +311,10 @@
           -> m ExitCode
   setHLS' SetOptions{ sToolVer } =
     case sToolVer of
-      (SetToolVersion v) -> runSetHLS runLeanAppState (liftE $ setHLS (_tvVersion v) >> pure v)
+      (SetToolVersion v) -> runSetHLS runLeanAppState (liftE $ setHLS (_tvVersion v) SetHLSOnly Nothing >> pure v)
       _ -> runSetHLS runAppState (do
           v <- liftE $ fst <$> fromVersion' sToolVer HLS
-          liftE $ setHLS (_tvVersion v)
+          liftE $ setHLS (_tvVersion v) SetHLSOnly Nothing
           pure v
         )
       >>= \case
diff --git a/app/ghcup/GHCup/OptParse/Upgrade.hs b/app/ghcup/GHCup/OptParse/Upgrade.hs
--- a/app/ghcup/GHCup/OptParse/Upgrade.hs
+++ b/app/ghcup/GHCup/OptParse/Upgrade.hs
@@ -113,17 +113,17 @@
 
 
 upgrade :: ( Monad m
-         , MonadMask m
-         , MonadUnliftIO m
-         , MonadFail m
-         )
+           , MonadMask m
+           , MonadUnliftIO m
+           , MonadFail m
+           )
         => UpgradeOpts
         -> Bool
+        -> Dirs
         -> (forall a. ReaderT AppState m (VEither UpgradeEffects a) -> m (VEither UpgradeEffects a))
         -> (ReaderT LeanAppState m () -> m ())
         -> m ExitCode
-upgrade uOpts force' runAppState runLogger = do
-  VRight Dirs{ .. }  <- runAppState (VRight <$> getDirs)
+upgrade uOpts force' Dirs{..} runAppState runLogger = do
   target <- case uOpts of
     UpgradeInplace  -> Just <$> liftIO getExecutablePath
     (UpgradeAt p)   -> pure $ Just p
diff --git a/app/ghcup/Main.hs b/app/ghcup/Main.hs
--- a/app/ghcup/Main.hs
+++ b/app/ghcup/Main.hs
@@ -140,7 +140,12 @@
         <> hidden
         )
   let listCommands = infoOption
-        "install set rm install-cabal list upgrade compile debug-info tool-requirements changelog"
+        ("install set rm install-cabal list"
+#ifndef DISABLE_UPGRADE
+          <> " upgrade"
+#endif
+          <> " compile debug-info tool-requirements changelog"
+        )
         (  long "list-commands"
         <> help "List available commands for shell completion"
         <> internal
@@ -238,10 +243,14 @@
                              alreadyInstalling' <- alreadyInstalling optCommand newTool
                              when (not alreadyInstalling') $
                                case t of
+#ifdef DISABLE_UPGRADE
+                                 GHCup -> pure ()
+#else
                                  GHCup -> runLogger $
                                             logWarn ("New GHCup version available: "
                                               <> prettyVer l
                                               <> ". To upgrade, run 'ghcup upgrade'")
+#endif
                                  _ -> runLogger $
                                         logWarn ("New "
                                           <> T.pack (prettyShow t)
@@ -296,12 +305,15 @@
             Config configCommand     -> config configCommand settings keybindings runLogger
             Whereis whereisOptions
                     whereisCommand   -> whereis whereisCommand whereisOptions runAppState leanAppstate runLogger
-            Upgrade uOpts force'     -> upgrade uOpts force' runAppState runLogger
+#ifndef DISABLE_UPGRADE
+            Upgrade uOpts force'     -> upgrade uOpts force' dirs runAppState runLogger
+#endif
             ToolRequirements         -> toolRequirements runAppState runLogger
             ChangeLog changelogOpts  -> changelog changelogOpts runAppState runLogger
             Nuke                     -> nuke appState runLogger
             Prefetch pfCom           -> prefetch pfCom runAppState runLogger
             GC gcOpts                -> gc gcOpts runAppState runLogger
+            Run runCommand           -> run runCommand runAppState leanAppstate runLogger
 
           case res of
             ExitSuccess        -> pure ()
@@ -339,6 +351,9 @@
     (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
+#ifndef DISABLE_UPGRADE
+  alreadyInstalling (Upgrade _ _) (GHCup, _) = pure True
+#endif
   alreadyInstalling _ _ = pure False
 
   cmp' :: ( HasLog env
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.4
+version:            0.1.17.5
 license:            LGPL-3.0-only
 license-file:       LICENSE
 copyright:          Julian Ospald 2020
@@ -48,6 +48,13 @@
   default:     False
   manual:      True
 
+flag disable-upgrade
+  description:
+    Build the brick powered tui (ghcup tui). This is disabled on windows.
+
+  default:     False
+  manual:      True
+
 library
   exposed-modules:
     GHCup
@@ -58,6 +65,7 @@
     GHCup.Requirements
     GHCup.Types
     GHCup.Types.JSON
+    GHCup.Types.JSON.Utils
     GHCup.Types.Optics
     GHCup.Utils
     GHCup.Utils.Dirs
@@ -166,11 +174,10 @@
       GHCup.Utils.File.Posix
       GHCup.Utils.Posix
       GHCup.Utils.Prelude.Posix
-      System.Console.Terminal.Common
-      System.Console.Terminal.Posix
 
     build-depends:
       , bz2              >=0.5.0.5 && <1.1
+      , terminal-size    ^>=0.3.2.1
       , unix             ^>=2.7
       , unix-bytestring  ^>=0.3.7.3
 
@@ -193,10 +200,10 @@
     GHCup.OptParse.Nuke
     GHCup.OptParse.Prefetch
     GHCup.OptParse.Rm
+    GHCup.OptParse.Run
     GHCup.OptParse.Set
     GHCup.OptParse.ToolRequirements
     GHCup.OptParse.UnSet
-    GHCup.OptParse.Upgrade
     GHCup.OptParse.Whereis
 
   hs-source-dirs:     app/ghcup
@@ -237,6 +244,7 @@
     , resourcet             ^>=1.2.2
     , safe                  ^>=0.3.18
     , safe-exceptions       ^>=0.1
+    , temporary             ^>=1.3
     , template-haskell      >=2.7      && <2.18
     , text                  ^>=1.2.4.0
     , uri-bytestring        ^>=0.3.2.2
@@ -259,9 +267,18 @@
 
   if os(windows)
     cpp-options: -DIS_WINDOWS
+  else
+    build-depends:
+      , unix             ^>=2.7
 
   if flag(no-exe)
     buildable: False
+
+  if (flag(disable-upgrade))
+    cpp-options:   -DDISABLE_UPGRADE
+  else
+    other-modules:
+        GHCup.OptParse.Upgrade
 
 test-suite ghcup-test
   type:               exitcode-stdio-1.0
diff --git a/lib/GHCup.hs b/lib/GHCup.hs
--- a/lib/GHCup.hs
+++ b/lib/GHCup.hs
@@ -5,8 +5,8 @@
 {-# LANGUAGE FlexibleInstances     #-}
 {-# LANGUAGE MultiParamTypeClasses #-}
 {-# LANGUAGE OverloadedStrings     #-}
-{-# LANGUAGE TemplateHaskell       #-}
 {-# LANGUAGE QuasiQuotes           #-}
+{-# LANGUAGE TemplateHaskell       #-}
 
 {-|
 Module      : GHCup
@@ -301,24 +301,8 @@
   liftE $ runBuildAction tmpUnpack
                          (Just inst)
                          (installUnpackedGHC workdir inst ver)
- where
-  -- | Does basic checks for isolated installs
-  -- Isolated Directory:
-  --   1. if it doesn't exist -> proceed
-  --   2. if it exists and is empty -> proceed
-  --   3. if it exists and is non-empty -> panic and leave the house
-  installDestSanityCheck :: ( MonadIO m
-                            , MonadCatch m
-                            ) =>
-                            FilePath ->
-                            Excepts '[DirNotEmpty] m ()
-  installDestSanityCheck isoDir = do
-    hideErrorDef [doesNotExistErrorType] () $ do
-      contents <- liftIO $ getDirectoryContentsRecursive isoDir
-      unless (null contents) (throwE $ DirNotEmpty isoDir)
 
 
-
 -- | Install an unpacked GHC distribution. This only deals with the GHC
 -- build system and nothing else.
 installUnpackedGHC :: ( MonadReader env m
@@ -582,6 +566,8 @@
                         , TarDirDoesNotExist
                         , ArchiveResult
                         , FileAlreadyExistsError
+                        , ProcessError
+                        , DirNotEmpty
                         ]
                        m
                        ()
@@ -617,29 +603,58 @@
 
   -- the subdir of the archive where we do the work
   workdir <- maybe (pure tmpUnpack) (liftE . intoSubdir tmpUnpack) (view dlSubdir dlinfo)
+  legacy <- liftIO $ isLegacyHLSBindist workdir
 
+  if
+    | not forceInstall
+    , not legacy
+    , (Just fp) <- isoFilepath -> liftE $ installDestSanityCheck fp
+    | otherwise -> pure ()
+
   case isoFilepath of
     Just isoDir -> do
       lift $ logInfo $ "isolated installing HLS to " <> T.pack isoDir
-      liftE $ installHLSUnpacked workdir isoDir Nothing forceInstall
+      if legacy
+      then liftE $ installHLSUnpackedLegacy workdir isoDir Nothing forceInstall
+      else liftE $ runBuildAction tmpUnpack Nothing $ installHLSUnpacked workdir isoDir ver
 
     Nothing -> do
-      liftE $ installHLSUnpacked workdir binDir (Just ver) forceInstall
+      if legacy
+      then liftE $ installHLSUnpackedLegacy workdir binDir (Just ver) forceInstall
+      else do
+        inst <- ghcupHLSDir ver
+        liftE $ runBuildAction tmpUnpack Nothing $ installHLSUnpacked workdir inst ver
+        liftE $ setHLS ver SetHLS_XYZ Nothing
 
   liftE $ installHLSPostInst isoFilepath ver
 
+isLegacyHLSBindist :: FilePath -- ^ Path to the unpacked hls bindist
+                   -> IO Bool
+isLegacyHLSBindist path = do
+  not <$> doesFileExist (path </> "GNUmakefile")
 
 -- | Install an unpacked hls distribution.
-installHLSUnpacked :: (MonadReader env m, MonadFail m, HasLog env, MonadCatch m, MonadIO m)
-              => FilePath      -- ^ Path to the unpacked hls bindist (where the executable resides)
-              -> FilePath      -- ^ Path to install to
-              -> Maybe Version -- ^ Nothing for isolated install
-              -> Bool          -- ^ is it a force install
-              -> Excepts '[CopyError, FileAlreadyExistsError] m ()
-installHLSUnpacked path inst mver' forceInstall = do
+installHLSUnpacked :: (MonadMask m, MonadUnliftIO m, MonadReader env m, MonadFail m, HasLog env, HasDirs env, HasSettings env, MonadCatch m, MonadIO m)
+                   => FilePath      -- ^ Path to the unpacked hls bindist (where the executable resides)
+                   -> FilePath      -- ^ Path to install to
+                   -> Version
+                   -> Excepts '[ProcessError, CopyError, FileAlreadyExistsError, NotInstalled] m ()
+installHLSUnpacked path inst _ = do
   lift $ logInfo "Installing HLS"
   liftIO $ createDirRecursive' inst
+  lEM $ make ["PREFIX=" <> inst, "install"] (Just path)
 
+-- | Install an unpacked hls distribution (legacy).
+installHLSUnpackedLegacy :: (MonadReader env m, MonadFail m, HasLog env, MonadCatch m, MonadIO m)
+                         => FilePath      -- ^ Path to the unpacked hls bindist (where the executable resides)
+                         -> FilePath      -- ^ Path to install to
+                         -> Maybe Version -- ^ Nothing for isolated install
+                         -> Bool          -- ^ is it a force install
+                         -> Excepts '[CopyError, FileAlreadyExistsError] m ()
+installHLSUnpackedLegacy path inst mver' forceInstall = do
+  lift $ logInfo "Installing HLS"
+  liftIO $ createDirRecursive' inst
+
   -- install haskell-language-server-<ghcver>
   bins@(_:_) <- liftIO $ findFiles
     path
@@ -692,7 +707,7 @@
       -- create symlink if this is the latest version in a regular install
       hlsVers <- lift $ fmap rights getInstalledHLSs
       let lInstHLS = headMay . reverse . sort $ hlsVers
-      when (maybe True (ver >=) lInstHLS) $ liftE $ setHLS ver
+      when (maybe True (ver >=) lInstHLS) $ liftE $ setHLS ver SetHLSOnly Nothing
 
 
 -- | Installs hls binaries @haskell-language-server-\<ghcver\>@
@@ -725,6 +740,8 @@
                     , TarDirDoesNotExist
                     , ArchiveResult
                     , FileAlreadyExistsError
+                    , ProcessError
+                    , DirNotEmpty
                     ]
                    m
                    ()
@@ -894,9 +911,9 @@
       case isolateDir of
         Just isoDir -> do
           lift $ logInfo $ "isolated installing HLS to " <> T.pack isoDir
-          liftE $ installHLSUnpacked installDir isoDir Nothing True
+          liftE $ installHLSUnpackedLegacy installDir isoDir Nothing True
         Nothing -> do
-          liftE $ installHLSUnpacked installDir binDir (Just installVer) True
+          liftE $ installHLSUnpackedLegacy installDir binDir (Just installVer) True
     )
 
   liftE $ installHLSPostInst isolateDir installVer
@@ -1075,22 +1092,29 @@
           )
        => GHCTargetVersion
        -> SetGHC
+       -> Maybe FilePath  -- if set, signals that we're not operating in ~/.ghcup/bin
+                          -- and don't want mess with other versions
        -> Excepts '[NotInstalled] m GHCTargetVersion
-setGHC ver sghc = do
+setGHC ver sghc mBinDir = do
   let verS = T.unpack $ prettyVer (_tvVersion ver)
   ghcdir                        <- lift $ ghcupGHCDir ver
 
   whenM (lift $ not <$> ghcInstalled ver) (throwE (NotInstalled GHC ver))
 
   -- symlink destination
-  Dirs {..} <- lift getDirs
+  binDir <- case mBinDir of
+    Just x -> pure x
+    Nothing -> do
+      Dirs {binDir = f} <- lift getDirs
+      pure f
 
   -- first delete the old symlinks (this fixes compatibility issues
   -- with old ghcup)
-  case sghc of
-    SetGHCOnly -> liftE $ rmPlain (_tvTarget ver)
-    SetGHC_XY  -> liftE $ rmMajorSymlinks ver
-    SetGHC_XYZ -> liftE $ rmMinorSymlinks ver
+  when (isNothing mBinDir) $
+    case sghc of
+      SetGHCOnly -> liftE $ rmPlainGHC (_tvTarget ver)
+      SetGHC_XY  -> liftE $ rmMajorGHCSymlinks ver
+      SetGHC_XYZ -> liftE $ rmMinorGHCSymlinks ver
 
   -- for ghc tools (ghc, ghci, haddock, ...)
   verfiles <- ghcToolFiles ver
@@ -1109,15 +1133,17 @@
 
     -- create symlink
     forM_ mTargetFile $ \targetFile -> do
+      bindir <- ghcInternalBinDir ver
       let fullF = binDir </> targetFile  <> exeExt
-          fileWithExt = file <> exeExt
-      destL <- lift $ ghcLinkDestination fileWithExt ver
+          fileWithExt = bindir </> file <> exeExt
+      destL <- binarySymLinkDestination binDir fileWithExt
       lift $ createLink destL fullF
 
-  -- create symlink for share dir
-  when (isNothing . _tvTarget $ ver) $ lift $ symlinkShareDir ghcdir verS
+  when (isNothing mBinDir) $ do
+    -- create symlink for share dir
+    when (isNothing . _tvTarget $ ver) $ lift $ symlinkShareDir ghcdir verS
 
-  when (sghc == SetGHCOnly) $ lift warnAboutHlsCompatibility
+    when (sghc == SetGHCOnly) $ lift warnAboutHlsCompatibility
 
   pure ver
 
@@ -1170,7 +1196,7 @@
             )
          => Maybe Text
          -> Excepts '[NotInstalled] m ()
-unsetGHC = rmPlain
+unsetGHC = rmPlainGHC
 
 
 -- | Set the @~\/.ghcup\/bin\/cabal@ symlink.
@@ -1222,37 +1248,62 @@
           , MonadUnliftIO m
           )
        => Version
+       -> SetHLS -- Nothing for legacy
+       -> Maybe FilePath  -- if set, signals that we're not operating in ~/.ghcup/bin
+                          -- and don't want mess with other versions
        -> Excepts '[NotInstalled] m ()
-setHLS ver = do
-  Dirs {..} <- lift getDirs
+setHLS ver shls mBinDir = do
+  whenM (lift $ not <$> hlsInstalled ver) (throwE (NotInstalled HLS (GHCTargetVersion Nothing ver)))
 
-  -- Delete old symlinks, since these might have different ghc versions than the
-  -- selected version, so we could end up with stray or incorrect symlinks.
-  oldSyms <- lift hlsSymlinks
-  forM_ oldSyms $ \f -> do
-    lift $ logDebug $ "rm " <> T.pack (binDir </> f)
-    lift $ rmLink (binDir </> f)
+  -- symlink destination
+  binDir <- case mBinDir of
+    Just x -> pure x
+    Nothing -> do
+      Dirs {binDir = f} <- lift getDirs
+      pure f
 
-  -- set haskell-language-server-<ghcver> symlinks
-  bins <- lift $ hlsServerBinaries ver Nothing
-  when (null bins) $ throwE $ NotInstalled HLS (GHCTargetVersion Nothing ver)
+  -- first delete the old symlinks
+  when (isNothing mBinDir) $
+    case shls of
+      -- not for legacy
+      SetHLS_XYZ -> liftE $ rmMinorHLSSymlinks ver
+      -- legacy and new
+      SetHLSOnly -> liftE rmPlainHLS
 
-  forM_ bins $ \f -> do
-    let destL = f
-    let target = (<> exeExt) . head . splitOn "~" $ f
-    lift $ createLink destL (binDir </> target)
+  case shls of
+    -- not for legacy
+    SetHLS_XYZ -> do
+      bins <- lift $ hlsInternalServerScripts ver Nothing
 
-  -- set haskell-language-server-wrapper symlink
-  let destL = "haskell-language-server-wrapper-" <> T.unpack (prettyVer ver) <> exeExt
-  let wrapper = binDir </> "haskell-language-server-wrapper" <> exeExt
+      forM_ bins $ \f -> do
+        let fname = takeFileName f
+        destL <- binarySymLinkDestination binDir f
+        let target = if "haskell-language-server-wrapper" `isPrefixOf` fname
+                     then fname <> "-" <> T.unpack (prettyVer ver) <> exeExt
+                     else fname <> "~" <> T.unpack (prettyVer ver) <> exeExt
+        lift $ createLink destL (binDir </> target)
 
-  lift $ createLink destL wrapper
+    -- legacy and new
+    SetHLSOnly -> do
+      -- set haskell-language-server-<ghcver> symlinks
+      bins <- lift $ hlsServerBinaries ver Nothing
+      when (null bins) $ throwE $ NotInstalled HLS (GHCTargetVersion Nothing ver)
 
-  lift warnAboutHlsCompatibility
+      forM_ bins $ \f -> do
+        let destL = f
+        let target = (<> exeExt) . head . splitOn "~" $ f
+        lift $ createLink destL (binDir </> target)
 
-  pure ()
+      -- set haskell-language-server-wrapper symlink
+      let destL = "haskell-language-server-wrapper-" <> T.unpack (prettyVer ver) <> exeExt
+      let wrapper = binDir </> "haskell-language-server-wrapper" <> exeExt
 
+      lift $ createLink destL wrapper
 
+      when (isNothing mBinDir) $
+        lift warnAboutHlsCompatibility
+
+
 unsetHLS :: ( MonadMask m
             , MonadReader env m
             , HasDirs env
@@ -1720,14 +1771,14 @@
   -- this isn't atomic, order matters
   when isSetGHC $ do
     lift $ logInfo "Removing ghc symlinks"
-    liftE $ rmPlain (_tvTarget ver)
+    liftE $ rmPlainGHC (_tvTarget ver)
 
   lift $ logInfo "Removing ghc-x.y.z symlinks"
-  liftE $ rmMinorSymlinks ver
+  liftE $ rmMinorGHCSymlinks ver
 
   lift $ logInfo "Removing/rewiring ghc-x.y symlinks"
   -- first remove
-  handle (\(_ :: ParseError) -> pure ()) $ liftE $ rmMajorSymlinks ver
+  handle (\(_ :: ParseError) -> pure ()) $ liftE $ rmMajorGHCSymlinks ver
   -- then fix them (e.g. with an earlier version)
 
   lift $ logInfo $ "Removing directory recursively: " <> T.pack dir
@@ -1739,7 +1790,7 @@
     $ fmap Just
     $ getMajorMinorV (_tvVersion ver)
   forM_ v' $ \(mj, mi) -> lift (getGHCForPVP (PVP (fromIntegral mj :| [fromIntegral mi])) (_tvTarget ver))
-    >>= mapM_ (\v -> liftE $ setGHC v SetGHC_XY)
+    >>= mapM_ (\v -> liftE $ setGHC v SetGHC_XY Nothing)
 
   Dirs {..} <- lift getDirs
 
@@ -1794,24 +1845,19 @@
 rmHLSVer ver = do
   whenM (lift $ fmap not $ hlsInstalled ver) $ throwE (NotInstalled HLS (GHCTargetVersion Nothing ver))
 
-  isHlsSet      <- lift hlsSet
-
-  Dirs {..} <- lift getDirs
+  isHlsSet <- lift hlsSet
 
-  bins <- lift $ hlsAllBinaries ver
-  forM_ bins $ \f -> lift $ recycleFile (binDir </> f)
+  liftE $ rmMinorHLSSymlinks ver
+  hlsDir <- ghcupHLSDir ver
+  recyclePathForcibly hlsDir
 
   when (Just ver == isHlsSet) $ do
     -- delete all set symlinks
-    oldSyms <- lift hlsSymlinks
-    forM_ oldSyms $ \f -> do
-      let fullF = binDir </> f
-      lift $ logDebug $ "rm " <> T.pack fullF
-      lift $ rmLink fullF
+    rmPlainHLS
     -- set latest hls
     hlsVers <- lift $ fmap rights getInstalledHLSs
     case headMay . reverse . sort $ hlsVers of
-      Just latestver -> setHLS latestver
+      Just latestver -> setHLS latestver SetHLSOnly Nothing
       Nothing        -> pure ()
 
 
@@ -2245,7 +2291,7 @@
       Nothing -> do
         reThrowAll GHCupSetError $ postGHCInstall installVer
         -- restore
-        when alreadySet $ liftE $ void $ setGHC installVer SetGHCOnly
+        when alreadySet $ liftE $ void $ setGHC installVer SetGHCOnly Nothing
         
       _ -> pure ()
 
@@ -2639,7 +2685,7 @@
                => GHCTargetVersion
                -> Excepts '[NotInstalled] m ()
 postGHCInstall ver@GHCTargetVersion {..} = do
-  void $ liftE $ setGHC ver SetGHC_XYZ
+  void $ liftE $ setGHC ver SetGHC_XYZ Nothing
 
   -- Create ghc-x.y symlinks. This may not be the current
   -- version, create it regardless.
@@ -2648,7 +2694,7 @@
     $ fmap Just
     $ getMajorMinorV _tvVersion
   forM_ v' $ \(mj, mi) -> lift (getGHCForPVP (PVP (fromIntegral mj :| [fromIntegral mi])) _tvTarget)
-    >>= mapM_ (\v -> liftE $ setGHC v SetGHC_XY)
+    >>= mapM_ (\v -> liftE $ setGHC v SetGHC_XY Nothing)
 
 
 -- | Reports the binary location of a given tool:
@@ -2687,7 +2733,11 @@
     HLS -> do
       whenM (lift $ fmap not $ hlsInstalled _tvVersion)
         $ throwE (NotInstalled HLS (GHCTargetVersion Nothing _tvVersion))
-      pure (binDir dirs </> "haskell-language-server-wrapper-" <> T.unpack (prettyVer _tvVersion) <> exeExt)
+      ifM (lift $ isLegacyHLS _tvVersion)
+        (pure (binDir dirs </> "haskell-language-server-wrapper-" <> T.unpack (prettyVer _tvVersion) <> exeExt))
+        $ do
+          bdir <- lift $ ghcupHLSDir _tvVersion
+          pure (bdir </> "bin" </> "haskell-language-server-wrapper" <> exeExt)
 
     Stack -> do
       whenM (lift $ fmap not $ stackInstalled _tvVersion)
@@ -2705,13 +2755,21 @@
                         Tool ->
                         Version ->
                         m Bool
+checkIfToolInstalled tool ver = checkIfToolInstalled' tool (mkTVer ver)
 
-checkIfToolInstalled tool ver =
+checkIfToolInstalled' :: ( MonadIO m
+                         , MonadReader env m
+                         , HasDirs env
+                         , MonadCatch m) =>
+                        Tool ->
+                        GHCTargetVersion ->
+                        m Bool
+checkIfToolInstalled' tool ver =
   case tool of
-    Cabal -> cabalInstalled ver
-    HLS   -> hlsInstalled ver
-    Stack -> stackInstalled ver
-    GHC   -> ghcInstalled $ mkTVer ver
+    Cabal -> cabalInstalled (_tvVersion ver)
+    HLS   -> hlsInstalled (_tvVersion ver)
+    Stack -> stackInstalled (_tvVersion ver)
+    GHC   -> ghcInstalled ver
     _     -> pure False
 
 throwIfFileAlreadyExists :: ( MonadIO m ) =>
@@ -2800,21 +2858,31 @@
               , HasLog env
               , MonadIO m
               , MonadMask m
+              , MonadFail m
+              , MonadUnliftIO m
               )
-           => m ()
+           => Excepts '[NotInstalled] m ()
 rmHLSNoGHC = do
   Dirs {..} <- getDirs
   ghcs <- fmap rights getInstalledGHCs
   hlses <- fmap rights getInstalledHLSs
   forM_ hlses $ \hls -> do
     hlsGHCs <- fmap mkTVer <$> hlsGHCVersions' hls
-    forM_ hlsGHCs $ \ghc -> do 
-      when (ghc `notElem` ghcs) $ do
-        bins <- hlsServerBinaries hls (Just $ _tvVersion ghc)
-        forM_ bins $ \bin -> do
-          let f = binDir </> bin
+    let candidates = filter (`notElem` ghcs) hlsGHCs
+    if (length hlsGHCs - length candidates) <= 0
+    then rmHLSVer hls
+    else
+      forM_ candidates $ \ghc -> do
+        bins1 <- fmap (binDir </>) <$> hlsServerBinaries hls (Just $ _tvVersion ghc)
+        bins2 <- ifM (isLegacyHLS hls) (pure []) $ do
+          shs <- hlsInternalServerScripts hls (Just $ _tvVersion ghc)
+          bins <- hlsInternalServerBinaries hls (Just $ _tvVersion ghc)
+          libs <- hlsInternalServerLibs hls (_tvVersion ghc)
+          pure (shs ++ bins ++ libs)
+        forM_ (bins1 ++ bins2) $ \f -> do
           logDebug $ "rm " <> T.pack f
           rmFile f
+    pure ()
 
 
 rmCache :: ( MonadReader env m
diff --git a/lib/GHCup/Platform.hs b/lib/GHCup/Platform.hs
--- a/lib/GHCup/Platform.hs
+++ b/lib/GHCup/Platform.hs
@@ -4,7 +4,7 @@
 {-# LANGUAGE FlexibleInstances     #-}
 {-# LANGUAGE OverloadedStrings     #-}
 {-# LANGUAGE QuasiQuotes           #-}
-{-# LANGUAGE TemplateHaskell       #-}
+{-# LANGUAGE TemplateHaskellQuotes #-}
 
 
 {-|
diff --git a/lib/GHCup/Types.hs b/lib/GHCup/Types.hs
--- a/lib/GHCup/Types.hs
+++ b/lib/GHCup/Types.hs
@@ -6,6 +6,7 @@
 {-# LANGUAGE OverloadedStrings #-}
 {-# LANGUAGE FlexibleContexts  #-}
 {-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE TemplateHaskell   #-}
 {-# LANGUAGE DuplicateRecordFields #-}
 
 {-|
@@ -30,12 +31,15 @@
 import           Data.List.NonEmpty             ( NonEmpty (..) )
 import           Data.Text                      ( Text )
 import           Data.Versions
-import           Text.PrettyPrint.HughesPJClass (Pretty, pPrint, text)
+import           GHC.IO.Exception               ( ExitCode )
+import           Optics                         ( makeLenses )
+import           Text.PrettyPrint.HughesPJClass (Pretty, pPrint, text, (<+>))
 import           URI.ByteString
 #if defined(BRICK)
 import           Graphics.Vty                   ( Key(..) )
 #endif
 
+import qualified Data.ByteString.Lazy          as BL
 import qualified Data.Text                     as T
 import qualified GHC.Generics                  as GHC
 
@@ -484,7 +488,11 @@
             | SetGHC_XYZ  -- ^ ghc-x.y.z
             deriving (Eq, Show)
 
+data SetHLS = SetHLSOnly  -- ^ unversioned 'hls'
+            | SetHLS_XYZ  -- ^ haskell-language-server-a.b.c~x.y.z, where a.b.c is GHC version and x.y.z is HLS version
+            deriving (Eq, Show)
 
+
 data PlatformResult = PlatformResult
   { _platform      :: Platform
   , _distroVersion :: Maybe Versioning
@@ -596,3 +604,27 @@
 
 instance NFData LoggerConfig where
   rnf (LoggerConfig !lcPrintDebug !_ !_ !fancyColors) = rnf (lcPrintDebug, fancyColors)
+
+data ProcessError = NonZeroExit Int FilePath [String]
+                  | PTerminated FilePath [String]
+                  | PStopped FilePath [String]
+                  | NoSuchPid FilePath [String]
+                  deriving Show
+
+instance Pretty ProcessError where
+  pPrint (NonZeroExit e exe args) =
+    text "Process" <+> pPrint exe <+> text "with arguments" <+> pPrint args <+> text "failed with exit code" <+> text (show e <> ".")
+  pPrint (PTerminated exe args) =
+    text "Process" <+> pPrint exe <+> text "with arguments" <+> pPrint args <+> text "terminated."
+  pPrint (PStopped exe args) =
+    text "Process" <+> pPrint exe <+> text "with arguments" <+> pPrint args <+> text "stopped."
+  pPrint (NoSuchPid exe args) =
+    text "Could not find PID for process running " <+> pPrint exe <+> text " with arguments " <+> text (show args) <+> text "."
+data CapturedProcess = CapturedProcess
+  { _exitCode :: ExitCode
+  , _stdOut   :: BL.ByteString
+  , _stdErr   :: BL.ByteString
+  }
+  deriving (Eq, Show)
+
+makeLenses ''CapturedProcess
diff --git a/lib/GHCup/Types/JSON.hs b/lib/GHCup/Types/JSON.hs
--- a/lib/GHCup/Types/JSON.hs
+++ b/lib/GHCup/Types/JSON.hs
@@ -22,10 +22,8 @@
 module GHCup.Types.JSON where
 
 import           GHCup.Types
+import           GHCup.Types.JSON.Utils
 import           GHCup.Utils.MegaParsec
-import           GHCup.Utils.Prelude
-import           GHCup.Utils.Logger             () -- TH is broken shite and needs GHCup.Utils.Logger for linking, although we don't depend on the file.
-                                                   -- This is due to the boot file.
 
 import           Control.Applicative            ( (<|>) )
 import           Data.Aeson              hiding (Key)
@@ -40,6 +38,7 @@
 
 import qualified Data.List.NonEmpty            as NE
 import qualified Data.Text                     as T
+import qualified Data.Text.Encoding.Error      as E
 import qualified Text.Megaparsec               as MP
 import qualified Text.Megaparsec.Char          as MPC
 
@@ -78,7 +77,7 @@
     x -> pure (UnknownTag x)
 
 instance ToJSON URI where
-  toJSON = toJSON . decUTF8Safe . serializeURIRef'
+  toJSON = toJSON . E.decodeUtf8With E.lenientDecode . serializeURIRef'
 
 instance FromJSON URI where
   parseJSON = withText "URL" $ \t ->
diff --git a/lib/GHCup/Types/JSON/Utils.hs b/lib/GHCup/Types/JSON/Utils.hs
new file mode 100644
--- /dev/null
+++ b/lib/GHCup/Types/JSON/Utils.hs
@@ -0,0 +1,17 @@
+{-|
+Module      : GHCup.Types.JSON.Utils
+Description : Utils for TH splices
+Copyright   : (c) Julian Ospald, 2020
+License     : LGPL-3.0
+Maintainer  : hasufell@hasufell.de
+Stability   : experimental
+Portability : portable
+-}
+
+module GHCup.Types.JSON.Utils where
+
+import qualified Data.Text                     as T
+
+removeLensFieldLabel :: String -> String
+removeLensFieldLabel str' =
+  maybe str' T.unpack . T.stripPrefix (T.pack "_") . T.pack $ str'
diff --git a/lib/GHCup/Utils.hs b/lib/GHCup/Utils.hs
--- a/lib/GHCup/Utils.hs
+++ b/lib/GHCup/Utils.hs
@@ -3,7 +3,6 @@
 {-# LANGUAGE FlexibleContexts      #-}
 {-# LANGUAGE OverloadedStrings     #-}
 {-# LANGUAGE QuasiQuotes           #-}
-{-# LANGUAGE TemplateHaskell       #-}
 {-# LANGUAGE TypeApplications      #-}
 {-# LANGUAGE ViewPatterns          #-}
 
@@ -125,31 +124,32 @@
     ------------------------
 
 
--- | The symlink destination of a ghc tool.
-ghcLinkDestination :: ( MonadReader env m
-                      , HasDirs env
-                      , MonadThrow m, MonadIO m)
-                   => FilePath -- ^ the tool, such as 'ghc', 'haddock' etc.
-                   -> GHCTargetVersion
-                   -> m FilePath
-ghcLinkDestination tool ver = do
-  Dirs {..}  <- getDirs
-  ghcd <- ghcupGHCDir ver
-  pure (relativeSymlink binDir (ghcd </> "bin" </> tool))
+-- | Create a relative symlink destination for the binary directory,
+-- given a target toolpath.
+binarySymLinkDestination :: ( MonadThrow m
+                            , MonadIO m
+                            )
+                         => FilePath -- ^ binary dir
+                         -> FilePath -- ^ the full toolpath
+                         -> m FilePath
+binarySymLinkDestination binDir toolPath = do
+  toolPath' <- liftIO $ canonicalizePath toolPath
+  binDir' <- liftIO $ canonicalizePath binDir
+  pure (relativeSymlink binDir' toolPath')
 
 
 -- | Removes the minor GHC symlinks, e.g. ghc-8.6.5.
-rmMinorSymlinks :: ( MonadReader env m
-                   , HasDirs env
-                   , MonadIO m
-                   , HasLog env
-                   , MonadThrow m
-                   , MonadFail m
-                   , MonadMask m
-                   )
-                => GHCTargetVersion
-                -> Excepts '[NotInstalled] m ()
-rmMinorSymlinks tv@GHCTargetVersion{..} = do
+rmMinorGHCSymlinks :: ( MonadReader env m
+                      , HasDirs env
+                      , MonadIO m
+                      , HasLog env
+                      , MonadThrow m
+                      , MonadFail m
+                      , MonadMask m
+                      )
+                   => GHCTargetVersion
+                   -> Excepts '[NotInstalled] m ()
+rmMinorGHCSymlinks tv@GHCTargetVersion{..} = do
   Dirs {..}  <- lift getDirs
 
   files                         <- liftE $ ghcToolFiles tv
@@ -161,17 +161,17 @@
 
 
 -- | Removes the set ghc version for the given target, if any.
-rmPlain :: ( MonadReader env m
-           , HasDirs env
-           , HasLog env
-           , MonadThrow m
-           , MonadFail m
-           , MonadIO m
-           , MonadMask m
-           )
-        => Maybe Text -- ^ target
-        -> Excepts '[NotInstalled] m ()
-rmPlain target = do
+rmPlainGHC :: ( MonadReader env m
+              , HasDirs env
+              , HasLog env
+              , MonadThrow m
+              , MonadFail m
+              , MonadIO m
+              , MonadMask m
+              )
+           => Maybe Text -- ^ target
+           -> Excepts '[NotInstalled] m ()
+rmPlainGHC target = do
   Dirs {..}  <- lift getDirs
   mtv                           <- lift $ ghcSet target
   forM_ mtv $ \tv -> do
@@ -187,17 +187,17 @@
 
 
 -- | Remove the major GHC symlink, e.g. ghc-8.6.
-rmMajorSymlinks :: ( MonadReader env m
-                   , HasDirs env
-                   , MonadIO m
-                   , HasLog env
-                   , MonadThrow m
-                   , MonadFail m
-                   , MonadMask m
-                   )
-                => GHCTargetVersion
-                -> Excepts '[NotInstalled] m ()
-rmMajorSymlinks tv@GHCTargetVersion{..} = do
+rmMajorGHCSymlinks :: ( MonadReader env m
+                      , HasDirs env
+                      , MonadIO m
+                      , HasLog env
+                      , MonadThrow m
+                      , MonadFail m
+                      , MonadMask m
+                      )
+                   => GHCTargetVersion
+                   -> Excepts '[NotInstalled] m ()
+rmMajorGHCSymlinks tv@GHCTargetVersion{..} = do
   Dirs {..}  <- lift getDirs
   (mj, mi) <- getMajorMinorV _tvVersion
   let v' = intToText mj <> "." <> intToText mi
@@ -210,8 +210,64 @@
     lift $ hideError doesNotExistErrorType $ rmLink fullF
 
 
+-- | Removes the minor HLS files, e.g. 'haskell-language-server-8.10.7~1.6.1.0'
+-- and 'haskell-language-server-wrapper-1.6.1.0'.
+rmMinorHLSSymlinks :: ( MonadReader env m
+                      , HasDirs env
+                      , MonadIO m
+                      , HasLog env
+                      , MonadThrow m
+                      , MonadFail m
+                      , MonadMask m
+                      )
+                   => Version
+                   -> Excepts '[NotInstalled] m ()
+rmMinorHLSSymlinks ver = do
+  Dirs {..}  <- lift getDirs
 
+  hlsBins <- hlsAllBinaries ver
+  forM_ hlsBins $ \f -> do
+    let fullF = binDir </> f
+    lift $ logDebug ("rm -f " <> T.pack fullF)
+    -- on unix, this may be either a file (legacy) or a symlink
+    -- on windows, this is always a file... hence 'rmFile'
+    -- works consistently across platforms
+    lift $ rmFile fullF
 
+-- | Removes the set HLS version, if any.
+rmPlainHLS :: ( MonadReader env m
+              , HasDirs env
+              , HasLog env
+              , MonadThrow m
+              , MonadFail m
+              , MonadIO m
+              , MonadMask m
+              )
+           => Excepts '[NotInstalled] m ()
+rmPlainHLS = do
+  Dirs {..}  <- lift getDirs
+
+  -- delete 'haskell-language-server-8.10.7'
+  hlsBins <- fmap (filter (\f -> not ("haskell-language-server-wrapper" `isPrefixOf` f) && ('~' `notElem` f)))
+    $ liftIO $ handleIO (\_ -> pure []) $ findFiles
+      binDir
+      (makeRegexOpts compExtended execBlank ([s|^haskell-language-server-.*$|] :: ByteString))
+  forM_ hlsBins $ \f -> do
+    let fullF = binDir </> f
+    lift $ logDebug ("rm -f " <> T.pack fullF)
+    if isWindows
+    then lift $ rmLink fullF
+    else lift $ rmFile fullF
+
+  -- 'haskell-language-server-wrapper'
+  let hlswrapper = binDir </> "haskell-language-server-wrapper" <> exeExt
+  lift $ logDebug ("rm -f " <> T.pack hlswrapper)
+  if isWindows
+  then lift $ hideError doesNotExistErrorType $ rmLink hlswrapper
+  else lift $ hideError doesNotExistErrorType $ rmFile hlswrapper
+
+
+
     -----------------------------------
     --[ Set/Installed introspection ]--
     -----------------------------------
@@ -353,7 +409,8 @@
 
 
 -- | Get all installed hls, by matching on
--- @~\/.ghcup\/bin/haskell-language-server-wrapper-<\hlsver\>@.
+-- @~\/.ghcup\/bin/haskell-language-server-wrapper-<\hlsver\>@,
+-- as well as @~\/.ghcup\/hls\/<\hlsver\>@
 getInstalledHLSs :: (MonadReader env m, HasDirs env, MonadIO m, MonadCatch m)
                  => m [Either FilePath Version]
 getInstalledHLSs = do
@@ -364,7 +421,7 @@
                    execBlank
                    ([s|^haskell-language-server-wrapper-.*$|] :: ByteString)
     )
-  forM bins $ \f ->
+  legacy <- forM bins $ \f ->
     case
           version . T.pack <$> (stripSuffix exeExt =<< stripPrefix "haskell-language-server-wrapper-" f)
       of
@@ -372,6 +429,14 @@
         Just (Left  _) -> pure $ Left f
         Nothing        -> pure $ Left f
 
+  hlsdir <- ghcupHLSBaseDir
+  fs     <- liftIO $ hideErrorDef [NoSuchThing] [] $ listDirectory hlsdir
+  new <- forM fs $ \f -> case parseGHCupHLSDir f of
+    Right r -> pure $ Right r
+    Left  _ -> pure $ Left f
+  pure (nub (new <> legacy))
+
+
 -- | Get all installed stacks, by matching on
 -- @~\/.ghcup\/bin/stack-<\stackver\>@.
 getInstalledStacks :: (MonadReader env m, HasDirs env, MonadIO m, MonadCatch m)
@@ -447,6 +512,10 @@
   vers <- fmap rights getInstalledHLSs
   pure $ elem ver vers
 
+isLegacyHLS :: (MonadIO m, MonadReader env m, HasDirs env, MonadCatch m) => Version -> m Bool
+isLegacyHLS ver = do
+  bdir <- ghcupHLSDir ver
+  not <$> liftIO (doesDirectoryExist bdir)
 
 
 -- Return the currently set hls version, if any.
@@ -518,7 +587,7 @@
   pure . sortBy (flip compare) . rights $ vers
 
 
--- | Get all server binaries for an hls version, if any.
+-- | Get all server binaries for an hls version from the ~/.ghcup/bin directory, if any.
 hlsServerBinaries :: (MonadReader env m, HasDirs env, MonadIO m)
                   => Version
                   -> Maybe Version   -- ^ optional GHC version
@@ -539,7 +608,45 @@
       )
     )
 
+-- | Get all scripts for a hls version from the ~/.ghcup/hls/<ver>/bin directory, if any.
+-- Returns the full path.
+hlsInternalServerScripts :: (MonadReader env m, HasDirs env, MonadIO m, MonadThrow m)
+                          => Version
+                          -> Maybe Version   -- ^ optional GHC version
+                          -> m [FilePath]
+hlsInternalServerScripts ver mghcVer = do
+  dir <- ghcupHLSDir ver
+  let bdir = dir </> "bin"
+  fmap (bdir </>) . filter (\f -> maybe True (\gv -> ("-" <> T.unpack (prettyVer gv)) `isSuffixOf` f) mghcVer)
+    <$> liftIO (listDirectory bdir)
 
+-- | Get all binaries for a hls version from the ~/.ghcup/hls/<ver>/lib/haskell-language-server-<ver>/bin directory, if any.
+-- Returns the full path.
+hlsInternalServerBinaries :: (MonadReader env m, HasDirs env, MonadIO m, MonadThrow m, MonadFail m)
+                          => Version
+                          -> Maybe Version   -- ^ optional GHC version
+                          -> m [FilePath]
+hlsInternalServerBinaries ver mghcVer = do
+  dir <- ghcupHLSDir ver
+  let regex = makeRegexOpts compExtended execBlank ([s|^haskell-language-server-.*$|] :: ByteString)
+  (Just bdir) <- fmap headMay $ liftIO $ expandFilePath [Left (dir </> "lib"), Right regex, Left "bin"]
+  fmap (bdir </>) . filter (\f -> maybe True (\gv -> ("-" <> T.unpack (prettyVer gv)) `isSuffixOf` f) mghcVer)
+    <$> liftIO (listDirectory bdir)
+
+-- | Get all libraries for a hls version from the ~/.ghcup/hls/<ver>/lib/haskell-language-server-<ver>/lib/<ghc-ver>/
+-- directory, if any.
+-- Returns the full path.
+hlsInternalServerLibs :: (MonadReader env m, HasDirs env, MonadIO m, MonadThrow m, MonadFail m)
+                      => Version
+                      -> Version   -- ^ GHC version
+                      -> m [FilePath]
+hlsInternalServerLibs ver ghcVer = do
+  dir <- ghcupHLSDir ver
+  let regex = makeRegexOpts compExtended execBlank ([s|^haskell-language-server-.*$|] :: ByteString)
+  (Just bdir) <- fmap headMay $ liftIO $ expandFilePath [Left (dir </> "lib"), Right regex, Left ("lib" </> T.unpack (prettyVer ghcVer))]
+  fmap (bdir </>) <$> liftIO (listDirectory bdir)
+
+
 -- | Get the wrapper binary for an hls version, if any.
 hlsWrapperBinary :: (MonadReader env m, HasDirs env, MonadThrow m, MonadIO m)
                  => Version
@@ -569,22 +676,6 @@
   pure (maybeToList wrapper ++ hls)
 
 
--- | Get the active symlinks for hls.
-hlsSymlinks :: (MonadReader env m, HasDirs env, MonadIO m, MonadCatch m) => m [FilePath]
-hlsSymlinks = do
-  Dirs {..}  <- getDirs
-  oldSyms                       <- liftIO $ handleIO (\_ -> pure []) $ findFiles
-    binDir
-    (makeRegexOpts compExtended
-                   execBlank
-                   ([s|^haskell-language-server-.*$|] :: ByteString)
-    )
-  filterM
-    ( liftIO
-    . pathIsLink
-    . (binDir </>)
-    )
-    oldSyms
 
 
 
@@ -715,7 +806,7 @@
       (untar . GZip.decompress =<< rf av)
     | ".tar.xz" `isSuffixOf` fn -> do
       filecontents <- liftE $ rf av
-      let decompressed = Lzma.decompress filecontents
+      let decompressed = Lzma.decompressWith (Lzma.defaultDecompressParams { Lzma.decompressAutoDecoder= True }) filecontents
       liftE $ untar decompressed
     | ".tar.bz2" `isSuffixOf` fn ->
       liftE (untar . BZip.decompress =<< rf av)
@@ -744,7 +835,7 @@
       (entries . GZip.decompress =<< rf av)
     | ".tar.xz" `isSuffixOf` fn -> do
       filecontents <- liftE $ rf av
-      let decompressed = Lzma.decompress filecontents
+      let decompressed = Lzma.decompressWith (Lzma.defaultDecompressParams { Lzma.decompressAutoDecoder= True }) filecontents
       liftE $ entries decompressed
     | ".tar.bz2" `isSuffixOf` fn ->
       liftE (entries . BZip.decompress =<< rf av)
@@ -809,8 +900,16 @@
     --[ Other ]--
     -------------
 
+-- | Usually @~\/.ghcup\/ghc\/\<ver\>\/bin\/@
+ghcInternalBinDir :: (MonadReader env m, HasDirs env, MonadThrow m, MonadFail m, MonadIO m)
+                  => GHCTargetVersion
+                  -> m FilePath
+ghcInternalBinDir ver = do
+  ghcdir <- ghcupGHCDir ver
+  pure (ghcdir </> "bin")
 
--- | Get tool files from @~\/.ghcup\/bin\/ghc\/\<ver\>\/bin\/\*@
+
+-- | Get tool files from @~\/.ghcup\/ghc\/\<ver\>\/bin\/\*@
 -- while ignoring @*-\<ver\>@ symlinks and accounting for cross triple prefix.
 --
 -- Returns unversioned relative files without extension, e.g.:
@@ -820,11 +919,10 @@
              => GHCTargetVersion
              -> Excepts '[NotInstalled] m [FilePath]
 ghcToolFiles ver = do
-  ghcdir <- lift $ ghcupGHCDir ver
-  let bindir = ghcdir </> "bin"
+  bindir <- ghcInternalBinDir ver
 
   -- fail if ghc is not installed
-  whenM (fmap not $ liftIO $ doesDirectoryExist ghcdir)
+  whenM (fmap not $ ghcInstalled ver)
         (throwE (NotInstalled GHC ver))
 
   files <- liftIO (listDirectory bindir >>= filterM (doesFileExist . (bindir </>)))
@@ -879,22 +977,30 @@
   executeOut mymake args workdir
 
 
--- | Try to apply patches in order. Fails with 'PatchFailed'
--- on first failure.
+-- | Try to apply patches in order. The order is determined by
+-- a quilt series file (in the patch directory) if one exists,
+-- else the patches are applied in lexicographical order.
+-- Fails with 'PatchFailed' on first failure.
 applyPatches :: (MonadReader env m, HasDirs env, HasLog env, MonadIO m)
              => FilePath   -- ^ dir containing patches
              -> FilePath   -- ^ dir to apply patches in
              -> Excepts '[PatchFailed] m ()
 applyPatches pdir ddir = do
-  patches <- (fmap . fmap) (pdir </>) $ liftIO $ findFiles
-      pdir
-      (makeRegexOpts compExtended
-                     execBlank
-                     ([s|.+\.(patch|diff)$|] :: ByteString)
-      )
-  forM_ (sort patches) $ \patch' -> applyPatch patch' ddir
+  let lexicographical = (fmap . fmap) (pdir </>) $ sort <$> findFiles
+        pdir
+        (makeRegexOpts compExtended
+                       execBlank
+                       ([s|.+\.(patch|diff)$|] :: ByteString)
+        )
+  let quilt = map (pdir </>) . lines <$> readFile (pdir </> "series")
 
+  patches <- liftIO $ quilt `catchIO` (\e ->
+    if isDoesNotExistError e || isPermissionError e then
+      lexicographical 
+    else throwIO e)
+  forM_ patches $ \patch' -> applyPatch patch' ddir
 
+
 applyPatch :: (MonadReader env m, HasDirs env, HasLog env, MonadIO m)
            => FilePath   -- ^ Patch
            -> FilePath   -- ^ dir to apply patches in
@@ -1141,11 +1247,27 @@
 
 -- | For ghc without arch triple, this is:
 --
---    - ghc-<ver> (e.g. ghc-8.10.4)
+--    - ghc
 --
 -- For ghc with arch triple:
 --
---    - <triple>-ghc-<ver> (e.g. arm-linux-gnueabihf-ghc-8.10.4)
+--    - <triple>-ghc (e.g. arm-linux-gnueabihf-ghc)
 ghcBinaryName :: GHCTargetVersion -> String
-ghcBinaryName (GHCTargetVersion (Just t) v') = T.unpack (t <> "-ghc-" <> prettyVer v' <> T.pack exeExt)
-ghcBinaryName (GHCTargetVersion Nothing  v') = T.unpack ("ghc-" <> prettyVer v' <> T.pack exeExt)
+ghcBinaryName (GHCTargetVersion (Just t) _) = T.unpack (t <> "-ghc" <> T.pack exeExt)
+ghcBinaryName (GHCTargetVersion Nothing  _) = T.unpack ("ghc" <> T.pack exeExt)
+
+
+-- | Does basic checks for isolated installs
+-- Isolated Directory:
+--   1. if it doesn't exist -> proceed
+--   2. if it exists and is empty -> proceed
+--   3. if it exists and is non-empty -> panic and leave the house
+installDestSanityCheck :: ( MonadIO m
+                          , MonadCatch m
+                          ) =>
+                          FilePath ->
+                          Excepts '[DirNotEmpty] m ()
+installDestSanityCheck isoDir = do
+  hideErrorDef [doesNotExistErrorType] () $ do
+    contents <- liftIO $ getDirectoryContentsRecursive isoDir
+    unless (null contents) (throwE $ DirNotEmpty isoDir)
diff --git a/lib/GHCup/Utils/Dirs.hs b/lib/GHCup/Utils/Dirs.hs
--- a/lib/GHCup/Utils/Dirs.hs
+++ b/lib/GHCup/Utils/Dirs.hs
@@ -20,8 +20,11 @@
   , ghcupCacheDir
   , ghcupGHCBaseDir
   , ghcupGHCDir
+  , ghcupHLSBaseDir
+  , ghcupHLSDir
   , mkGhcupTmpDir
   , parseGHCupGHCDir
+  , parseGHCupHLSDir
   , relativeSymlink
   , withGHCupTmpDir
   , getConfigFilePath
@@ -46,6 +49,7 @@
 import           Control.Monad.Trans.Resource hiding (throwM)
 import           Data.Bifunctor
 import           Data.Maybe
+import           Data.Versions
 import           GHC.IO.Exception               ( IOErrorType(NoSuchThing) )
 import           Haskus.Utils.Variant.Excepts
 import           Optics
@@ -244,7 +248,25 @@
 parseGHCupGHCDir (T.pack -> fp) =
   throwEither $ MP.parse ghcTargetVerP "" fp
 
+parseGHCupHLSDir :: MonadThrow m => FilePath -> m Version
+parseGHCupHLSDir (T.pack -> fp) =
+  throwEither $ MP.parse version' "" fp
 
+-- | ~/.ghcup/hls by default, for new-style installs.
+ghcupHLSBaseDir :: (MonadReader env m, HasDirs env) => m FilePath
+ghcupHLSBaseDir = do
+  Dirs {..}  <- getDirs
+  pure (baseDir </> "hls")
+
+-- | Gets '~/.ghcup/hls/<hls-ver>' for new-style installs.
+ghcupHLSDir :: (MonadReader env m, HasDirs env, MonadThrow m)
+            => Version
+            -> m FilePath
+ghcupHLSDir ver = do
+  basedir <- ghcupHLSBaseDir
+  let verdir = T.unpack $ prettyVer ver
+  pure (basedir </> verdir)
+
 mkGhcupTmpDir :: ( MonadReader env m
                  , HasDirs env
                  , MonadUnliftIO m
@@ -313,6 +335,7 @@
 useXDG = isJust <$> lookupEnv "GHCUP_USE_XDG_DIRS"
 
 
+-- | Like 'relpath'. Assumes the inputs are resolved in case of symlinks.
 relativeSymlink :: FilePath  -- ^ the path in which to create the symlink
                 -> FilePath  -- ^ the symlink destination
                 -> FilePath
diff --git a/lib/GHCup/Utils/File/Common.hs b/lib/GHCup/Utils/File/Common.hs
--- a/lib/GHCup/Utils/File/Common.hs
+++ b/lib/GHCup/Utils/File/Common.hs
@@ -1,11 +1,14 @@
 {-# LANGUAGE OverloadedStrings #-}
 {-# LANGUAGE FlexibleContexts  #-}
-{-# LANGUAGE TemplateHaskell   #-}
-{-# LANGUAGE ViewPatterns      #-}
 
-module GHCup.Utils.File.Common where
+module GHCup.Utils.File.Common (
+  module GHCup.Utils.File.Common
+  , ProcessError(..)
+  , CapturedProcess(..)
+  ) where
 
 import           GHCup.Utils.Prelude
+import           GHCup.Types(ProcessError(..), CapturedProcess(..))
 
 import           Control.Monad.Reader
 import           Data.Maybe
@@ -13,7 +16,7 @@
 import           Data.Void
 import           GHC.IO.Exception
 import           Optics                  hiding ((<|), (|>))
-import           System.Directory
+import           System.Directory        hiding (findFiles)
 import           System.FilePath
 import           Text.PrettyPrint.HughesPJClass hiding ( (<>) )
 import           Text.Regex.Posix
@@ -24,33 +27,6 @@
 
 
 
-data ProcessError = NonZeroExit Int FilePath [String]
-                  | PTerminated FilePath [String]
-                  | PStopped FilePath [String]
-                  | NoSuchPid FilePath [String]
-                  deriving Show
-
-instance Pretty ProcessError where
-  pPrint (NonZeroExit e exe args) =
-    text "Process" <+> pPrint exe <+> text "with arguments" <+> pPrint args <+> text "failed with exit code" <+> text (show e <> ".")
-  pPrint (PTerminated exe args) =
-    text "Process" <+> pPrint exe <+> text "with arguments" <+> pPrint args <+> text "terminated."
-  pPrint (PStopped exe args) =
-    text "Process" <+> pPrint exe <+> text "with arguments" <+> pPrint args <+> text "stopped."
-  pPrint (NoSuchPid exe args) =
-    text "Could not find PID for process running " <+> pPrint exe <+> text " with arguments " <+> text (show args) <+> text "."
-
-data CapturedProcess = CapturedProcess
-  { _exitCode :: ExitCode
-  , _stdOut   :: BL.ByteString
-  , _stdErr   :: BL.ByteString
-  }
-  deriving (Eq, Show)
-
-makeLenses ''CapturedProcess
-
-
-
 -- | Search for a file in the search paths.
 --
 -- Catches `PermissionDenied` and `NoSuchThing` and returns `Nothing`.
@@ -98,6 +74,21 @@
   if dir `elem` spaths
   then isJust <$> searchPath [dir] fn
   else pure False
+
+
+-- | Follows the first match in case of Regex.
+expandFilePath :: [Either FilePath Regex] -> IO [FilePath]
+expandFilePath = go ""
+ where
+  go :: FilePath -> [Either FilePath Regex] -> IO [FilePath]
+  go p [] = pure [p]
+  go p (x:xs) = do
+    case x of
+      Left s -> go (p </> s) xs
+      Right regex -> do
+        fps <- findFiles p regex
+        res <- forM fps $ \fp -> go (p </> fp) xs
+        pure $ mconcat res
 
 
 findFiles :: FilePath -> Regex -> IO [FilePath]
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
@@ -35,7 +35,7 @@
 import           Data.List
 import           Data.Word8
 import           GHC.IO.Exception
-import           System.Console.Terminal.Common
+import           System.IO                      ( stderr )
 import           System.IO.Error
 import           System.FilePath
 import           System.Directory
@@ -51,7 +51,7 @@
 import qualified Data.Text                     as T
 import qualified Data.Text.Encoding            as E
 import qualified System.Posix.Process          as SPP
-import qualified System.Console.Terminal.Posix as TP
+import qualified System.Console.Terminal.Size  as TP
 import qualified Data.ByteString               as BS
 import qualified Data.ByteString.Lazy          as BL
 import qualified "unix-bytestring" System.Posix.IO.ByteString
@@ -143,14 +143,14 @@
   printToRegion :: Fd -> Fd -> Int -> MVar Bool -> Bool -> IO ()
   printToRegion fileFd fdIn size pState no_color = do
     -- init region
-    forM_ [1..size] $ \_ -> BS.putStr "\n"
+    forM_ [1..size] $ \_ -> BS.hPut stderr "\n"
 
     void $ flip runStateT mempty
       $ do
         handle
           (\(ex :: SomeException) -> do
             ps <- liftIO $ takeMVar pState
-            when ps (liftIO $ BS.putStr (pos1 <> moveLineUp size <> clearScreen))
+            when ps (liftIO $ BS.hPut stderr (pos1 <> moveLineUp size <> clearScreen))
             throw ex
           ) $ readTilEOF lineAction fdIn
 
@@ -182,10 +182,10 @@
       modify (swapRegs bs')
       liftIO TP.size >>= \case
         Nothing -> pure ()
-        Just (Window _ w) -> do
+        Just (TP.Window _ w) -> do
           regs <- get
           liftIO $ forM_ (Sq.zip regs (Sq.fromList [0..(Sq.length regs - 1)])) $ \(bs, i) -> do
-              BS.putStr
+              BS.hPut stderr
               . overwriteNthLine (size - i)
               . trim w
               . blue
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
@@ -196,7 +196,8 @@
           then pure ()
           else do
             void $ BS.appendFile logFile some
-            void $ BS.hPut stdout some
+            -- subprocess stdout also goes to stderr for logging
+            void $ BS.hPut stderr some
             go
         
 
diff --git a/lib/GHCup/Utils/Logger.hs b/lib/GHCup/Utils/Logger.hs
--- a/lib/GHCup/Utils/Logger.hs
+++ b/lib/GHCup/Utils/Logger.hs
@@ -1,5 +1,4 @@
 {-# LANGUAGE FlexibleContexts #-}
-{-# LANGUAGE QuasiQuotes      #-}
 {-# LANGUAGE DataKinds        #-}
 {-# LANGUAGE OverloadedStrings   #-}
 
@@ -18,7 +17,7 @@
 
 import           GHCup.Types
 import           GHCup.Types.Optics
-import {-# SOURCE #-} GHCup.Utils.File.Common
+import {-# SOURCE #-} GHCup.Utils.File.Common (findFiles)
 import           GHCup.Utils.String.QQ
 
 import           Control.Exception.Safe
diff --git a/lib/GHCup/Utils/Logger.hs-boot b/lib/GHCup/Utils/Logger.hs-boot
--- a/lib/GHCup/Utils/Logger.hs-boot
+++ b/lib/GHCup/Utils/Logger.hs-boot
@@ -1,7 +1,5 @@
 {-# LANGUAGE FlexibleContexts #-}
-{-# LANGUAGE QuasiQuotes      #-}
 {-# LANGUAGE DataKinds        #-}
-{-# LANGUAGE OverloadedStrings   #-}
 
 module GHCup.Utils.Logger where
 
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
@@ -30,7 +30,7 @@
 import           GHCup.Types
 import           GHCup.Errors
 import           GHCup.Types.Optics
-import {-# SOURCE #-} GHCup.Utils.Logger
+import {-# SOURCE #-} GHCup.Utils.Logger (logWarn)
 #if defined(IS_WINDOWS)
 import           GHCup.Utils.Prelude.Windows
 #else
@@ -308,11 +308,6 @@
 intToText = TL.toStrict . B.toLazyText . B.decimal
 
 
-removeLensFieldLabel :: String -> String
-removeLensFieldLabel str' =
-  maybe str' T.unpack . T.stripPrefix (T.pack "_") . T.pack $ str'
-
-
 pvpToVersion :: MonadThrow m => PVP -> Text -> m Version
 pvpToVersion pvp_ rest =
   either (\_ -> throwM $ ParseError "Couldn't convert PVP to Version") pure . version . (<> rest) . prettyPVP $ pvp_
@@ -477,7 +472,9 @@
       let dest = tmp </> takeFileName fp
       liftIO (moveFile fp dest)
           `catch`
-          (\e -> if isPermissionError e {- EXDEV on windows -} then recover (liftIO $ removePathForcibly fp) else throwIO e)
+          (\e -> if | isDoesNotExistError e -> pure ()
+                    | isPermissionError e {- EXDEV on windows -} -> recover (liftIO $ removePathForcibly fp)
+                    | otherwise -> throwIO e)
           `finally`
             liftIO (handleIO (\_ -> pure ()) $ removePathForcibly tmp)
   | otherwise = liftIO $ removePathForcibly fp
diff --git a/lib/GHCup/Utils/String/QQ.hs b/lib/GHCup/Utils/String/QQ.hs
--- a/lib/GHCup/Utils/String/QQ.hs
+++ b/lib/GHCup/Utils/String/QQ.hs
@@ -1,4 +1,4 @@
-{-# LANGUAGE TemplateHaskell #-}
+{-# LANGUAGE TemplateHaskellQuotes #-}
 
 {-|
 Module      : GHCup.Utils.String.QQ
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
@@ -4,7 +4,7 @@
 {-# LANGUAGE DeriveLift         #-}
 {-# LANGUAGE FlexibleInstances  #-}
 {-# LANGUAGE StandaloneDeriving #-}
-{-# LANGUAGE TemplateHaskell    #-}
+{-# LANGUAGE TemplateHaskellQuotes #-}
 
 
 {-|
diff --git a/lib/GHCup/Version.hs b/lib/GHCup/Version.hs
--- a/lib/GHCup/Version.hs
+++ b/lib/GHCup/Version.hs
@@ -28,7 +28,7 @@
 -- Note that when updating this, CI requires that the file exsists AND the same file exists at
 -- 'https://www.haskell.org/ghcup/exp/ghcup-<ver>.yaml' with some newlines added.
 ghcupURL :: URI
-ghcupURL = [uri|https://raw.githubusercontent.com/haskell/ghcup-metadata/master/ghcup-0.0.6.yaml|]
+ghcupURL = [uri|https://raw.githubusercontent.com/haskell/ghcup-metadata/master/ghcup-0.0.7.yaml|]
 
 -- | The current ghcup version.
 ghcUpVer :: PVP
diff --git a/lib/System/Console/Terminal/Common.hs b/lib/System/Console/Terminal/Common.hs
deleted file mode 100644
--- a/lib/System/Console/Terminal/Common.hs
+++ /dev/null
@@ -1,43 +0,0 @@
-{-# LANGUAGE CPP #-}
-{-# LANGUAGE DeriveDataTypeable #-}
-{-# LANGUAGE DeriveTraversable #-}
-
-#if __GLASGOW_HASKELL__ >= 702
-#define LANGUAGE_DeriveGeneric
-{-# LANGUAGE DeriveGeneric #-}
-#endif
-
-module System.Console.Terminal.Common
-  ( Window(..)
-  ) where
-
-import Data.Data (Typeable, Data)
-
-#if __GLASGOW_HASKELL__ < 710
-import Data.Foldable (Foldable)
-import Data.Traversable (Traversable)
-#endif
-
-#ifdef LANGUAGE_DeriveGeneric
-import GHC.Generics
-  ( Generic
-#if __GLASGOW_HASKELL__ >= 706
-  , Generic1
-#endif
-  )
-#endif
-
--- | Terminal window width and height
-data Window a = Window
-  { height :: !a
-  , width  :: !a
-  } deriving
-    ( Show, Eq, Read, Data, Typeable
-    , Foldable, Functor, Traversable
-#ifdef LANGUAGE_DeriveGeneric
-    , Generic
-#if __GLASGOW_HASKELL__ >= 706
-    , Generic1
-#endif
-#endif
-    )
diff --git a/lib/System/Console/Terminal/Posix.hsc b/lib/System/Console/Terminal/Posix.hsc
deleted file mode 100644
--- a/lib/System/Console/Terminal/Posix.hsc
+++ /dev/null
@@ -1,65 +0,0 @@
-{-# LANGUAGE CApiFFI #-}
-
-module System.Console.Terminal.Posix
-  ( size, fdSize, hSize
-  ) where
-
-import System.Console.Terminal.Common
-import Control.Exception (catch)
-import Data.Typeable (cast)
-import Foreign
-import Foreign.C.Error
-import Foreign.C.Types
-import GHC.IO.FD (FD(FD, fdFD))
-import GHC.IO.Handle.Internals (withHandle_)
-import GHC.IO.Handle.Types (Handle, Handle__(Handle__, haDevice))
-#if defined(__GLASGOW_HASKELL__) && (__GLASGOW_HASKELL__ < 706)
-import Prelude hiding (catch)
-#endif
-import System.Posix.Types (Fd(Fd))
-
-#include <sys/ioctl.h>
-#include <unistd.h>
-
-
-#let alignment t = "%lu", (unsigned long)offsetof(struct {char x__; t (y__); }, y__)
-
-
--- Interesting part of @struct winsize@
-data CWin = CWin CUShort CUShort
-
-instance Storable CWin where
-  sizeOf _ = (#size struct winsize)
-  alignment _ = (#alignment struct winsize)
-  peek ptr = do
-    row <- (#peek struct winsize, ws_row) ptr
-    col <- (#peek struct winsize, ws_col) ptr
-    return $ CWin row col
-  poke ptr (CWin row col) = do
-    (#poke struct winsize, ws_row) ptr row
-    (#poke struct winsize, ws_col) ptr col
-
-
-fdSize :: Integral n => Fd -> IO (Maybe (Window n))
-fdSize (Fd fd) = with (CWin 0 0) $ \ws -> do
-  _ <- throwErrnoIfMinus1 "ioctl" $
-    ioctl fd (#const TIOCGWINSZ) ws
-  CWin row col <- peek ws
-  return . Just $ Window (fromIntegral row) (fromIntegral col)
- `catch`
-  handler
- where
-  handler :: IOError -> IO (Maybe (Window h))
-  handler _ = return Nothing
-
-foreign import capi "sys/ioctl.h ioctl"
-  ioctl :: CInt -> CULong -> Ptr CWin -> IO CInt
-
-size :: Integral n => IO (Maybe (Window n))
-size = fdSize (Fd (#const STDOUT_FILENO))
-
-hSize :: Integral n => Handle -> IO (Maybe (Window n))
-hSize h = withHandle_ "hSize" h $ \Handle__ { haDevice = dev } ->
-  case cast dev of
-    Nothing -> return Nothing
-    Just FD { fdFD = fd } -> fdSize (Fd fd)
