ghcup 0.1.22.0 → 0.1.30.0
raw patch · 49 files changed
+3838/−1758 lines, 49 filesdep +Cabal-syntaxdep +conduitdep +parsecdep −cabal-install-parsersdep −cabal-plandep −streamlydep ~asyncdep ~deepseqdep ~unordered-containers
Dependencies added: Cabal-syntax, conduit, parsec, yaml
Dependencies removed: cabal-install-parsers, cabal-plan, streamly, yaml-streamly
Dependency ranges changed: async, deepseq, unordered-containers
Files
- CHANGELOG.md +19/−0
- README.md +2/−2
- app/ghcup/BrickMain.hs +0/−1046
- app/ghcup/Main.hs +6/−4
- data/config.yaml +9/−0
- ghcup.cabal +64/−15
- lib-opt/GHCup/OptParse.hs +2/−2
- lib-opt/GHCup/OptParse/Common.hs +9/−371
- lib-opt/GHCup/OptParse/Compile.hs +34/−11
- lib-opt/GHCup/OptParse/Config.hs +3/−1
- lib-opt/GHCup/OptParse/Install.hs +1/−0
- lib-opt/GHCup/OptParse/List.hs +1/−0
- lib-opt/GHCup/OptParse/Prefetch.hs +1/−0
- lib-opt/GHCup/OptParse/Run.hs +1/−0
- lib-opt/GHCup/OptParse/Set.hs +1/−0
- lib-opt/GHCup/OptParse/Test.hs +1/−0
- lib-opt/GHCup/OptParse/Whereis.hs +1/−0
- lib-tui/GHCup/Brick/Actions.hs +738/−0
- lib-tui/GHCup/Brick/App.hs +193/−0
- lib-tui/GHCup/Brick/Attributes.hs +84/−0
- lib-tui/GHCup/Brick/BrickState.hs +54/−0
- lib-tui/GHCup/Brick/Common.hs +225/−0
- lib-tui/GHCup/Brick/Widgets/KeyInfo.hs +72/−0
- lib-tui/GHCup/Brick/Widgets/Menu.hs +381/−0
- lib-tui/GHCup/Brick/Widgets/Menus/AdvanceInstall.hs +118/−0
- lib-tui/GHCup/Brick/Widgets/Menus/CompileGHC.hs +219/−0
- lib-tui/GHCup/Brick/Widgets/Menus/CompileHLS.hs +190/−0
- lib-tui/GHCup/Brick/Widgets/Menus/Context.hs +83/−0
- lib-tui/GHCup/Brick/Widgets/Navigation.hs +149/−0
- lib-tui/GHCup/Brick/Widgets/SectionList.hs +193/−0
- lib-tui/GHCup/Brick/Widgets/Tutorial.hs +77/−0
- lib-tui/GHCup/BrickMain.hs +79/−0
- lib/GHCup.hs +4/−3
- lib/GHCup/CabalConfig.hs +111/−0
- lib/GHCup/Download/IOStreams.hs +8/−3
- lib/GHCup/GHC.hs +55/−40
- lib/GHCup/PlanJson.hs +79/−0
- lib/GHCup/Prelude/File.hs +15/−37
- lib/GHCup/Prelude/File/Posix.hs +33/−72
- lib/GHCup/Prelude/File/Search.hs +10/−0
- lib/GHCup/Prelude/File/Windows.hs +48/−88
- lib/GHCup/Prelude/Process/Windows.hs +27/−21
- lib/GHCup/Types.hs +33/−29
- lib/GHCup/Utils.hs +4/−2
- lib/GHCup/Utils/Dirs.hs +1/−0
- lib/GHCup/Utils/Parsers.hs +392/−0
- test/ghcup-test/GHCup/Utils/FileSpec.hs +3/−9
- test/optparse-test/CompileTest.hs +4/−2
- test/optparse-test/SetTest.hs +1/−0
CHANGELOG.md view
@@ -1,5 +1,24 @@ # Revision history for ghcup +## 0.1.30.0 -- 2024-07-07++### New features++* TUI: new context menus for advanced installation and compilation by @lsmor [#949](https://github.com/haskell/ghcup-hs/pull/949)+* Add `def-ghc-conf-options` config option++### Improvements and bug fixes++* Fix TUI for screenreaders+* Fix windows process creation [#1036](https://github.com/haskell/ghcup-hs/issues/1036)+* Migrate from streamly to conduit to unbreak hackage releases, wrt [#1051](https://github.com/haskell/ghcup-hs/issues/1051)+* Improve parsing of --platform values, fixes [#1057](https://github.com/haskell/ghcup-hs/issues/1057)+* Avoid option clash on `-s`+* Don't allow to specify --hadrian and --config wrt [#1082](https://github.com/haskell/ghcup-hs/issues/1082)+* Point to correct store dir when XDG is used [#1089](https://github.com/haskell/ghcup-hs/issues/1089)+* Fix boot GHC for `ghcup compile ghc` wrt [#1045](https://github.com/haskell/ghcup-hs/issues/1045)+* Add hadrian-ghc flag wrt [#1044](https://github.com/haskell/ghcup-hs/issues/1044)+ ## 0.1.22.0 -- 2024-03-13 ### New features
README.md view
@@ -1,8 +1,8 @@ ## The GHCup Haskell installer [](https://kiwiirc.com/nextclient/irc.libera.chat/?nick=Guest%7C?#haskell,#haskell-ghcup)-[](https://app.element.io/#/room/#haskell-tooling:matrix.org)-[](https://discord.gg/pKYf3zDQU7)+[](https://matrix.to/#/#ghcup:matrix.org)+[](https://discord.gg/WDqsWsnZfR) [](https://gitter.im/haskell/ghcup?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) <a href="https://opencollective.com/ghcup#category-CONTRIBUTE"><img src="https://opencollective.com/webpack/donate/button@2x.png?color=blue" alt="Donate" width="150"></a>
− app/ghcup/BrickMain.hs
@@ -1,1046 +0,0 @@-{-# LANGUAGE CPP #-}-{-# LANGUAGE DataKinds #-}-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE TypeApplications #-}-{-# LANGUAGE RankNTypes #-}-{-# LANGUAGE ViewPatterns #-}-{-# OPTIONS_GHC -Wno-unused-record-wildcards #-}-{-# OPTIONS_GHC -Wno-unused-matches #-}-{-# LANGUAGE FlexibleInstances #-}-{-# LANGUAGE MultiParamTypeClasses #-}-{-# LANGUAGE TemplateHaskell #-}-{-# LANGUAGE BangPatterns #-}-{-# LANGUAGE ViewPatterns #-}--module BrickMain where--import GHCup-import GHCup.Download-import GHCup.Errors-import GHCup.Types.Optics ( getDirs, getPlatformReq )-import GHCup.Types hiding ( LeanAppState(..) )-import GHCup.Utils-import GHCup.OptParse.Common (logGHCPostRm)-import GHCup.Prelude ( decUTF8Safe )-import GHCup.Prelude.Logger-import GHCup.Prelude.Process-import GHCup.Prompts--import Brick- ( BrickEvent(VtyEvent, MouseDown),- App(..),- Padding(Max, Pad),- AttrMap,- EventM,- Size(..),- Widget(..), - ViewportType (Vertical), - (<+>),- (<=>))-import qualified Brick-import Brick.Widgets.Border ( hBorder, borderWithLabel)-import Brick.Widgets.Border.Style ( unicode )-import Brick.Widgets.Center ( center, centerLayer )-import qualified Brick.Widgets.List as L-import Brick.Focus (FocusRing)-import qualified Brick.Focus as F-import Control.Applicative-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-import Data.Bool-import Data.Functor-import Data.Function ( (&), on)-import Data.List-import Data.Maybe-import Data.IORef (IORef, readIORef, newIORef, writeIORef, modifyIORef)-import Data.Vector ( Vector- - )-import Data.Versions hiding (Lens')-import Haskus.Utils.Variant.Excepts-import Prelude hiding ( appendFile )-import System.Exit-import System.IO.Unsafe-import System.Process ( system )-import Text.PrettyPrint.HughesPJClass ( prettyShow )-import URI.ByteString--import qualified Data.Text as T-import qualified Data.Text.Lazy.Builder as B-import qualified Data.Text.Lazy as L-import qualified Graphics.Vty as Vty-import qualified Data.Vector as V-import System.Environment (getExecutablePath)-#if !IS_WINDOWS-import GHCup.Prelude.File-import System.FilePath-import qualified System.Posix.Process as SPP-#endif--import Optics.TH (makeLenses, makeLensesFor)-import Optics.State (use)-import Optics.State.Operators ( (.=), (%=), (<%=))-import Optics.Operators ((.~), (^.), (%~))-import Optics.Getter (view)-import Optics.Lens (Lens', lens, toLensVL)--{- Brick's widget:-It is a FocusRing over many list's. Each list contains the information for each tool. Each list has an internal name (for Brick's runtime)-and a label which we can use in rendering. This data-structure helps to reuse Brick.Widget.List and to navegate easily across--Consider this code as private. GenericSectionList should not be used directly as the FocusRing should be align with the Vector containing -the elements, otherwise you'd be focusing on a non-existent widget with unknown result (In theory the code is safe unless you have an empty section list). --- To build a SectionList use the safe constructor sectionList-- To access sections use the lens provider sectionL and the name of the section you'd like to access-- You can modify Brick.Widget.List.GenericList within GenericSectionList via sectionL but do not- modify the vector length---}--data GenericSectionList n t e- = GenericSectionList- { sectionListFocusRing :: FocusRing n -- ^ The FocusRing for all sections- , sectionListElements :: !(Vector (L.GenericList n t e)) -- ^ A key-value vector- , sectionListName :: n -- ^ The section list name- }--makeLensesFor [("sectionListFocusRing", "sectionListFocusRingL"), ("sectionListElements", "sectionListElementsL"), ("sectionListName", "sectionListNameL")] ''GenericSectionList--type SectionList n e = GenericSectionList n V.Vector e----- | Build a SectionList from nonempty list. If empty we could not defined sectionL lenses. -sectionList :: Foldable t - => n -- The name of the section list- -> [(n, t e)] -- a list of tuples (section name, collection of elements)- -> Int- -> GenericSectionList n t e-sectionList name elements height- = GenericSectionList- { sectionListFocusRing = F.focusRing [section_name | (section_name, _) <- elements]- , sectionListElements = V.fromList [L.list section_name els height | (section_name, els) <- elements]- , sectionListName = name- }--- | This lens constructor, takes a name and looks if a section has such a name.--- Used to dispatch events to sections. It is a partial function only meant to --- be used with the FocusRing inside GenericSectionList-sectionL :: Eq n => n -> Lens' (GenericSectionList n t e) (L.GenericList n t e)-sectionL section_name = lens g s- where is_section_name = (== section_name) . L.listName- g section_list =- let elms = section_list ^. sectionListElementsL- zeroth = elms V.! 0 -- TODO: This crashes for empty vectors. - in fromMaybe zeroth (V.find is_section_name elms)- s gl@(GenericSectionList _ elms _) list =- case V.findIndex is_section_name elms of- Nothing -> gl- Just i -> let new_elms = V.update elms (V.fromList [(i, list)])- in gl & sectionListElementsL .~ new_elms--moveDown :: (L.Splittable t, Ord n, Foldable t) => EventM n (GenericSectionList n t e) ()-moveDown = do - ring <- use sectionListFocusRingL- case F.focusGetCurrent ring of - Nothing -> pure ()- Just l -> do -- If it is the last element, move to the first element of the next focus; else, just handle regular list event.- current_list <- use (sectionL l)- let current_idx = L.listSelected current_list- list_length = current_list & length- if current_idx == Just (list_length - 1)- then do - new_focus <- sectionListFocusRingL <%= F.focusNext- case F.focusGetCurrent new_focus of- Nothing -> pure () -- |- Optic.Zoom.zoom doesn't typecheck but Lens.Micro.Mtl.zoom does. It is re-exported by Brick- Just new_l -> Brick.zoom (toLensVL $ sectionL new_l) (Brick.modify L.listMoveToBeginning)- else Brick.zoom (toLensVL $ sectionL l) $ Brick.modify L.listMoveDown--moveUp :: (L.Splittable t, Ord n, Foldable t) => EventM n (GenericSectionList n t e) ()-moveUp = do- ring <- use sectionListFocusRingL- case F.focusGetCurrent ring of- Nothing -> pure ()- Just l -> do -- If it is the first element, move to the last element of the prev focus; else, just handle regular list event.- current_list <- use (sectionL l)- let current_idx = L.listSelected current_list- if current_idx == Just 0- then do - new_focus <- sectionListFocusRingL <%= F.focusPrev- case F.focusGetCurrent new_focus of- Nothing -> pure () - Just new_l -> Brick.zoom (toLensVL $ sectionL new_l) (Brick.modify L.listMoveToEnd)- else Brick.zoom (toLensVL $ sectionL l) $ Brick.modify L.listMoveUp---- | Handle events for list cursor movement. Events handled are:------ * Up (up arrow key). If first element of section, then jump prev section--- * Down (down arrow key). If last element of section, then jump next section--- * Page Up (PgUp)--- * Page Down (PgDown)--- * Go to next section (Tab)--- * Go to prev section (BackTab)-handleGenericListEvent :: (Foldable t, L.Splittable t, Ord n)- => BrickEvent n a- -> EventM n (GenericSectionList n t e) ()-handleGenericListEvent (VtyEvent (Vty.EvResize _ _)) = pure ()-handleGenericListEvent (VtyEvent (Vty.EvKey (Vty.KChar '\t') [])) = sectionListFocusRingL %= F.focusNext-handleGenericListEvent (VtyEvent (Vty.EvKey Vty.KBackTab [])) = sectionListFocusRingL %= F.focusPrev-handleGenericListEvent (MouseDown _ Vty.BScrollDown _ _) = moveDown-handleGenericListEvent (MouseDown _ Vty.BScrollUp _ _) = moveUp-handleGenericListEvent (VtyEvent (Vty.EvKey Vty.KDown [])) = moveDown-handleGenericListEvent (VtyEvent (Vty.EvKey Vty.KUp [])) = moveUp-handleGenericListEvent (VtyEvent ev) = do- ring <- use sectionListFocusRingL- case F.focusGetCurrent ring of- Nothing -> pure ()- Just l -> Brick.zoom (toLensVL $ sectionL l) $ L.handleListEvent ev-handleGenericListEvent _ = pure ()---- This re-uses Brick.Widget.List.renderList-renderSectionList :: forall n t e . (Traversable t, Ord n, Show n, Eq n, L.Splittable t, Semigroup (t e))- => (Bool -> e -> Widget n) -- ^ Rendering function of the list element, True for the selected element- -> Bool -- ^ Whether the section list has focus- -> GenericSectionList n t e -- ^ The section list to render- -> Widget n-renderSectionList renderElem sectionFocus ge@(GenericSectionList focus elms slName) =- Brick.Widget Brick.Greedy Brick.Greedy $ Brick.render $ Brick.viewport slName Brick.Vertical $- V.ifoldl' (\(!accWidget) !i list ->- let hasFocusList = sectionIsFocused list- makeVisible = if hasFocusList then Brick.visibleRegion (Brick.Location (c, r)) (1, 1) else id- appendBorder = if i == 0 then id else (hBorder <=>)- newWidget = appendBorder (makeVisible $ renderInnerList hasFocusList list)- in accWidget <=> newWidget- )- Brick.emptyWidget- elms- where- -- A section is focused if the whole thing is focused, and the inner list has focus- sectionIsFocused :: L.GenericList n t e -> Bool- sectionIsFocused l = sectionFocus && (Just (L.listName l) == F.focusGetCurrent focus)-- renderInnerList :: Bool -> L.GenericList n t e -> Widget n- renderInnerList hasFocus l = Brick.vLimit (length l) $ L.renderList (\b -> renderElem (b && hasFocus)) hasFocus l-- -- compute the location to focus on within the active section- (c, r) :: (Int, Int) = case sectionListSelectedElement ge of- Nothing -> (0, 0)- Just (selElIx, _) -> (0, selElIx)----- | Equivalent to listSelectedElement-sectionListSelectedElement :: (Eq n, L.Splittable t, Traversable t, Semigroup (t e)) => GenericSectionList n t e -> Maybe (Int, e)-sectionListSelectedElement generic_section_list = do- current_focus <- generic_section_list ^. sectionListFocusRingL & F.focusGetCurrent - let current_section = generic_section_list ^. sectionL current_focus- L.listSelectedElement current_section --{- Brick app data structures.--In this section we define the state, the widgets and the core data structures which we will be using for the brick app.---}--data Name = AllTools -- The main list widget- | Singular Tool -- The particular list for each tool- | KeyInfoBox -- The text box widget with action informacion- | TutorialBox -- The tutorial widget- deriving (Eq, Ord, Show)--data Mode = Navigation | KeyInfo | Tutorial deriving (Eq, Show, Ord)--installedSign :: String-#if IS_WINDOWS-installedSign = "I "-#else-installedSign = "✓ "-#endif--setSign :: String-#if IS_WINDOWS-setSign = "IS"-#else-setSign = "✔✔"-#endif--notInstalledSign :: String-#if IS_WINDOWS-notInstalledSign = "X "-#else-notInstalledSign = "✗ "-#endif---data BrickData = BrickData- { _lr :: [ListResult]- }- deriving Show--makeLenses ''BrickData--data BrickSettings = BrickSettings { _showAllVersions :: Bool}- --deriving Show--makeLenses ''BrickSettings--type BrickInternalState = SectionList Name ListResult--data BrickState = BrickState- { _appData :: BrickData- , _appSettings :: BrickSettings- , _appState :: BrickInternalState- , _appKeys :: KeyBindings- , _mode :: Mode- }- --deriving Show--makeLenses ''BrickState--app :: AttrMap -> AttrMap -> App BrickState () Name-app attrs dimAttrs =- App { appDraw = drawUI dimAttrs- , appHandleEvent = eventHandler- , appStartEvent = return ()- , appAttrMap = const attrs- , appChooseCursor = Brick.showFirstCursor- }---{- Drawing.--The section for creating our widgets. ---}--showKey :: Vty.Key -> String-showKey (Vty.KChar c) = [c]-showKey Vty.KUp = "↑"-showKey Vty.KDown = "↓"-showKey key = tail (show key)--showMod :: Vty.Modifier -> String-showMod = tail . show---drawNavigation :: AttrMap -> BrickState -> Widget Name-drawNavigation dimAttrs BrickState{ _appSettings = as@BrickSettings{}, ..}- = Brick.padBottom Max- ( Brick.withBorderStyle unicode- $ borderWithLabel (Brick.str "GHCup")- (center (header <=> hBorder <=> renderList' _appState))- )- <=> footer- where- footer =- Brick.withAttr helpAttr- . Brick.txtWrap- . T.pack- . foldr1 (\x y -> x <> " " <> y)- . fmap (\(KeyCombination key mods, s, _) -> intercalate "+" (showKey key : (showMod <$> mods)) <> ":" <> s as)- $ keyHandlers _appKeys- header =- minHSize 2 Brick.emptyWidget- <+> Brick.padLeft (Pad 2) (minHSize 6 $ Brick.str "Tool")- <+> minHSize 15 (Brick.str "Version")- <+> Brick.padLeft (Pad 1) (minHSize 25 $ Brick.str "Tags")- <+> Brick.padLeft (Pad 5) (Brick.str "Notes")- renderList' bis = - let allElements = V.concatMap L.listElements $ sectionListElements bis- minTagSize = V.maximum $ V.map (length . intercalate "," . fmap tagToString . lTag) allElements- minVerSize = V.maximum $ V.map (\ListResult{..} -> T.length $ tVerToText (GHCTargetVersion lCross lVer)) allElements- in Brick.withDefAttr L.listAttr $ renderSectionList (renderItem minTagSize minVerSize) True bis- renderItem minTagSize minVerSize b listResult@ListResult{lTag = lTag', ..} =- let marks = if- | lSet -> (Brick.withAttr setAttr $ Brick.str setSign)- | lInstalled -> (Brick.withAttr installedAttr $ Brick.str installedSign)- | otherwise -> (Brick.withAttr notInstalledAttr $ Brick.str notInstalledSign)- ver = case lCross of- Nothing -> T.unpack . prettyVer $ lVer- Just c -> T.unpack (c <> "-" <> prettyVer lVer)- dim- | lNoBindist && not lInstalled- && not b -- TODO: overloading dim and active ignores active- -- so we hack around it here- = Brick.updateAttrMap (const dimAttrs) . Brick.withAttr (Brick.attrName "no-bindist")- | otherwise = id- hooray- | elem Latest lTag' && not lInstalled =- Brick.withAttr hoorayAttr- | otherwise = id- in hooray $ dim- ( marks- <+> Brick.padLeft (Pad 2)- ( minHSize 6- (printTool lTool)- )- <+> minHSize minVerSize (Brick.str ver)- <+> (let l = catMaybes . fmap printTag $ sort lTag'- in Brick.padLeft (Pad 1) $ minHSize minTagSize $ if null l- then Brick.emptyWidget- else foldr1 (\x y -> x <+> Brick.str "," <+> y) l- )- <+> Brick.padLeft (Pad 5)- ( let notes = printNotes listResult- in if null notes- then Brick.emptyWidget- else foldr1 (\x y -> x <+> Brick.str "," <+> y) notes- )- <+> Brick.vLimit 1 (Brick.fill ' ')- )-- printTag Recommended = Just $ Brick.withAttr recommendedAttr $ Brick.str "recommended"- printTag Latest = Just $ Brick.withAttr latestAttr $ Brick.str "latest"- printTag Prerelease = Just $ Brick.withAttr prereleaseAttr $ Brick.str "prerelease"- printTag Nightly = Just $ Brick.withAttr nightlyAttr $ Brick.str "nightly"- printTag (Base pvp'') = Just $ Brick.str ("base-" ++ T.unpack (prettyPVP pvp''))- printTag Old = Nothing- printTag LatestPrerelease = Just $ Brick.withAttr latestPrereleaseAttr $ Brick.str "latest-prerelease"- printTag LatestNightly = Just $ Brick.withAttr latestNightlyAttr $ Brick.str "latest-nightly"- printTag (UnknownTag t) = Just $ Brick.str t-- printTool Cabal = Brick.str "cabal"- printTool GHC = Brick.str "GHC"- printTool GHCup = Brick.str "GHCup"- printTool HLS = Brick.str "HLS"- printTool Stack = Brick.str "Stack"-- printNotes ListResult {..} =- (if hlsPowered then [Brick.withAttr hlsPoweredAttr $ Brick.str "hls-powered"] else mempty- )- ++ (if lStray then [Brick.withAttr strayAttr $ Brick.str "stray"] else mempty)- ++ (case lReleaseDay of- Nothing -> mempty- Just d -> [Brick.withAttr dayAttr $ Brick.str (show d)])-- minHSize s' = Brick.hLimit s' . Brick.vLimit 1 . (<+> Brick.fill ' ')--drawTutorial :: Widget Name-drawTutorial = - let- mkTextBox = Brick.hLimitPercent 70 . Brick.vBox . fmap (Brick.padRight Brick.Max)- txt_separator = hBorder <+> Brick.str " o " <+> hBorder- in centerLayer- $ Brick.hLimitPercent 75- $ Brick.vLimitPercent 50- $ Brick.withBorderStyle unicode- $ borderWithLabel (Brick.txt "Tutorial")- $ Brick.vBox - (fmap center- [ mkTextBox [Brick.txtWrap "GHCup is a distribution channel for Haskell's tools."]- , txt_separator- , mkTextBox [- Brick.hBox [- Brick.txt "This symbol "- , Brick.withAttr installedAttr (Brick.str installedSign)- , Brick.txtWrap " means that the tool is installed but not in used"- ]- , Brick.hBox [- Brick.txt "This symbol "- , Brick.withAttr setAttr (Brick.str setSign)- , Brick.txtWrap " means that the tool is installed and in used"- ]- , Brick.hBox [- Brick.txt "This symbol "- , Brick.withAttr notInstalledAttr (Brick.str notInstalledSign)- , Brick.txt " means that the tool isn't installed"- ]- ]- , txt_separator- , mkTextBox [- Brick.hBox [- Brick.withAttr recommendedAttr $ Brick.str "recommended"- , Brick.txtWrap " tag is based on community adoption, known bugs, etc... So It makes this version the least experimental"- ]- , Brick.hBox [- Brick.withAttr latestAttr $ Brick.str "latest"- , Brick.txtWrap " tag is for the latest distributed version of the tool"- ]- , Brick.hBox [- Brick.withAttr latestAttr $ Brick.str "hls-powered"- , Brick.txt " denotes the compiler version supported by the currently set ("- , Brick.withAttr setAttr (Brick.str setSign)- , Brick.txt ") hls"- ]- , Brick.txtWrap "base-X.Y.Z.W tag is the minimun version of the base package admited in such ghc version"- ]- , Brick.txt " " - ])- <=> Brick.padRight Brick.Max (Brick.txt "Press q to exit the tutorial")--drawKeyInfo :: KeyBindings -> Widget Name-drawKeyInfo KeyBindings {..} =- let- mkTextBox = Brick.hLimitPercent 70 . Brick.vBox . fmap (Brick.padRight Brick.Max)- keyToWidget (KeyCombination key mods) = Brick.str $ intercalate "+" (showKey key : (showMod <$> mods))- in centerLayer- $ Brick.hLimitPercent 75- $ Brick.vLimitPercent 50- $ Brick.withBorderStyle unicode- $ borderWithLabel (Brick.txt "Key Actions")- $ Brick.vBox [- center $- mkTextBox [- Brick.hBox [- Brick.txt "Press "- , keyToWidget bUp, Brick.txt " and ", keyToWidget bDown- , Brick.txtWrap " to navigate the list of tools"- ]- , Brick.hBox [- Brick.txt "Press "- , keyToWidget bInstall- , Brick.txtWrap " to install the selected tool. Notice, you may need to set it as default afterwards"- ]- , Brick.hBox [- Brick.txt "Press "- , keyToWidget bSet- , Brick.txtWrap " to set a tool as the one for use"- ]- , Brick.hBox [- Brick.txt "Press "- , keyToWidget bUninstall- , Brick.txtWrap " to uninstall a tool"- ]- , Brick.hBox [- Brick.txt "Press "- , keyToWidget bChangelog- , Brick.txtWrap " to open the tool's changelog. It will open a web browser"- ]- , Brick.hBox [- Brick.txt "Press "- , keyToWidget bShowAllVersions- , Brick.txtWrap " to show older version of each tool"- ]- ]- ]- <=> Brick.hBox [Brick.txt "Press q to return to Navigation" <+> Brick.padRight Brick.Max (Brick.txt " ") <+> Brick.txt "Press Enter to go to the Tutorial"]--drawUI :: AttrMap -> BrickState -> [Widget Name]-drawUI dimAttrs st = - let navg = drawNavigation dimAttrs st- in case st ^. mode of- Navigation -> [navg]- Tutorial -> [drawTutorial, navg]- KeyInfo -> [drawKeyInfo (st ^. appKeys), navg]--{- Attributes---}---defaultAttributes :: Bool -> AttrMap-defaultAttributes no_color = Brick.attrMap- Vty.defAttr- [ (L.listSelectedFocusedAttr , Vty.defAttr `withBackColor` Vty.blue)- , (L.listSelectedAttr , Vty.defAttr)- , (notInstalledAttr , Vty.defAttr `withForeColor` Vty.red)- , (setAttr , Vty.defAttr `withForeColor` Vty.green)- , (installedAttr , Vty.defAttr `withForeColor` Vty.green)- , (recommendedAttr , Vty.defAttr `withForeColor` Vty.green)- , (hlsPoweredAttr , Vty.defAttr `withForeColor` Vty.green)- , (latestAttr , Vty.defAttr `withForeColor` Vty.yellow)- , (latestPrereleaseAttr , Vty.defAttr `withForeColor` Vty.red)- , (latestNightlyAttr , Vty.defAttr `withForeColor` Vty.red)- , (prereleaseAttr , Vty.defAttr `withForeColor` Vty.red)- , (nightlyAttr , Vty.defAttr `withForeColor` Vty.red)- , (compiledAttr , Vty.defAttr `withForeColor` Vty.blue)- , (strayAttr , Vty.defAttr `withForeColor` Vty.blue)- , (dayAttr , Vty.defAttr `withForeColor` Vty.blue)- , (helpAttr , Vty.defAttr `withStyle` Vty.italic)- , (hoorayAttr , Vty.defAttr `withForeColor` Vty.brightWhite)- ]- where- withForeColor | no_color = const- | otherwise = Vty.withForeColor-- withBackColor | no_color = \attr _ -> attr `Vty.withStyle` Vty.reverseVideo- | otherwise = Vty.withBackColor-- withStyle = Vty.withStyle---notInstalledAttr, setAttr, installedAttr, recommendedAttr, hlsPoweredAttr:: Brick.AttrName-latestAttr, latestPrereleaseAttr, latestNightlyAttr, prereleaseAttr, nightlyAttr:: Brick.AttrName-compiledAttr, strayAttr, dayAttr, helpAttr, hoorayAttr:: Brick.AttrName--notInstalledAttr = Brick.attrName "not-installed"-setAttr = Brick.attrName "set" -installedAttr = Brick.attrName "installed" -recommendedAttr = Brick.attrName "recommended" -hlsPoweredAttr = Brick.attrName "hls-powered"-latestAttr = Brick.attrName "latest" -latestPrereleaseAttr = Brick.attrName "latest-prerelease"-latestNightlyAttr = Brick.attrName "latest-nightly"-prereleaseAttr = Brick.attrName "prerelease" -nightlyAttr = Brick.attrName "nightly" -compiledAttr = Brick.attrName "compiled" -strayAttr = Brick.attrName "stray" -dayAttr = Brick.attrName "day" -helpAttr = Brick.attrName "help" -hoorayAttr = Brick.attrName "hooray" --dimAttributes :: Bool -> AttrMap-dimAttributes no_color = Brick.attrMap- (Vty.defAttr `Vty.withStyle` Vty.dim)- [ (Brick.attrName "active" , Vty.defAttr `withBackColor` Vty.blue) -- has no effect ??- , (Brick.attrName "no-bindist", Vty.defAttr `Vty.withStyle` Vty.dim)- ]- where- withBackColor | no_color = \attr _ -> attr `Vty.withStyle` Vty.reverseVideo- | otherwise = Vty.withBackColor--{- Handlers---}--keyHandlers :: KeyBindings- -> [ ( KeyCombination- , BrickSettings -> String- , EventM Name BrickState ()- )- ]-keyHandlers KeyBindings {..} =- [ (bQuit, const "Quit" , Brick.halt)- , (bInstall, const "Install" , withIOAction install')- , (bUninstall, const "Uninstall", withIOAction del')- , (bSet, const "Set" , withIOAction set')- , (bChangelog, const "ChangeLog", withIOAction changelog')- , ( bShowAllVersions- , \BrickSettings {..} ->- if _showAllVersions then "Don't show all versions" else "Show all versions"- , hideShowHandler' (not . _showAllVersions)- )- , (bUp, const "Up", Brick.zoom (toLensVL appState) moveUp)- , (bDown, const "Down", Brick.zoom (toLensVL appState) moveDown)- , (KeyCombination (Vty.KChar 'h') [], const "help", mode .= KeyInfo)- ]- where- --hideShowHandler' :: (BrickSettings -> Bool) -> (BrickSettings -> Bool) -> m ()- hideShowHandler' f = do - app_settings <- use appSettings- let - vers = f app_settings- newAppSettings = app_settings & showAllVersions .~ vers- ad <- use appData- current_app_state <- use appState- appSettings .= newAppSettings- appState .= constructList ad newAppSettings (Just current_app_state)---tutorialHandler :: BrickEvent Name e -> EventM Name BrickState ()-tutorialHandler ev = - case ev of- VtyEvent (Vty.EvKey (Vty.KChar 'q') _ ) -> mode .= Navigation- _ -> pure ()--keyInfoHandler :: BrickEvent Name e -> EventM Name BrickState ()-keyInfoHandler ev = do- case ev of- VtyEvent (Vty.EvKey (Vty.KChar 'q') _ ) -> mode .= Navigation- VtyEvent (Vty.EvKey Vty.KEnter _ ) -> mode .= Tutorial- _ -> pure ()--navigationHandler :: BrickEvent Name e -> EventM Name BrickState ()-navigationHandler ev = do- AppState { keyBindings = kb } <- liftIO $ readIORef settings'- case ev of- inner_event@(VtyEvent (Vty.EvKey key _)) ->- case find (\(key', _, _) -> key' == KeyCombination key []) (keyHandlers kb) of- Nothing -> void $ Brick.zoom (toLensVL appState) $ handleGenericListEvent inner_event- Just (_, _, handler) -> handler- inner_event -> Brick.zoom (toLensVL appState) $ handleGenericListEvent inner_event--eventHandler :: BrickEvent Name e -> EventM Name BrickState ()-eventHandler ev = do- m <- use mode- case m of- KeyInfo -> keyInfoHandler ev- Tutorial -> tutorialHandler ev- Navigation -> navigationHandler ev---{- Core Logic. --This section defines the IO actions we can execute within the Brick App:- - Install- - Set- - UnInstall- - Launch the Changelog---}---- | Suspend the current UI and run an IO action in terminal. If the--- IO action returns a Left value, then it's thrown as userError.-withIOAction :: (Ord n, Eq n)- => ( (Int, ListResult) -> ReaderT AppState IO (Either String a))- -> EventM n BrickState ()-withIOAction action = do- as <- Brick.get- case sectionListSelectedElement (view appState as) of- Nothing -> pure ()- Just (curr_ix, e) -> do- Brick.suspendAndResume $ do- settings <- readIORef settings'- flip runReaderT settings $ action (curr_ix, e) >>= \case- Left err -> liftIO $ putStrLn ("Error: " <> err)- Right _ -> liftIO $ putStrLn "Success"- getAppData Nothing >>= \case- Right data' -> do- putStrLn "Press enter to continue"- _ <- getLine- pure (updateList data' as)- Left err -> throwIO $ userError err----- | Update app data and list internal state based on new evidence.--- This synchronises @BrickInternalState@ with @BrickData@--- and @BrickSettings@.-updateList :: BrickData -> BrickState -> BrickState-updateList appD BrickState{..} =- let newInternalState = constructList appD _appSettings (Just _appState)- in BrickState { _appState = newInternalState- , _appData = appD- , _appSettings = _appSettings- , _appKeys = _appKeys- , _mode = Navigation- }--constructList :: BrickData- -> BrickSettings- -> Maybe BrickInternalState- -> BrickInternalState-constructList appD settings =- replaceLR (filterVisible (_showAllVersions settings))- (_lr appD)---- | Focus on the tool section and the predicate which matches. If no result matches, focus on index 0-selectBy :: Tool -> (ListResult -> Bool) -> BrickInternalState -> BrickInternalState-selectBy tool predicate internal_state =- let new_focus = F.focusSetCurrent (Singular tool) (view sectionListFocusRingL internal_state)- tool_lens = sectionL (Singular tool) - in internal_state- & sectionListFocusRingL .~ new_focus- & tool_lens %~ L.listMoveTo 0 -- We move to 0 first- & tool_lens %~ L.listFindBy predicate -- The lookup by the predicate.---- | Select the latests GHC tool-selectLatest :: BrickInternalState -> BrickInternalState-selectLatest = selectBy GHC (elem Latest . lTag)----- | Replace the @appState@ or construct it based on a filter function--- and a new @[ListResult]@ evidence.--- When passed an existing @appState@, tries to keep the selected element.-replaceLR :: (ListResult -> Bool)- -> [ListResult]- -> Maybe BrickInternalState- -> BrickInternalState-replaceLR filterF list_result s =- let oldElem = s >>= sectionListSelectedElement -- Maybe (Int, e)- newVec = [(Singular $ lTool (head g), V.fromList g) | g <- groupBy ((==) `on` lTool ) (filter filterF list_result)]- newSectionList = sectionList AllTools newVec 1- in case oldElem of- Just (_, el) -> selectBy (lTool el) (toolEqual el) newSectionList- Nothing -> selectLatest newSectionList- where- toolEqual e1 e2 =- lTool e1 == lTool e2 && lVer e1 == lVer e2 && lCross e1 == lCross e2---filterVisible :: Bool -> ListResult -> Bool-filterVisible v e | lInstalled e = True- | v- , Nightly `notElem` lTag e = True- | not v- , Old `notElem` lTag e- , Nightly `notElem` lTag e = True- | otherwise = (Old `notElem` lTag e) &&- (Nightly `notElem` lTag e)---install' :: (MonadReader AppState m, MonadIO m, MonadThrow m, MonadFail m, MonadMask m, MonadUnliftIO m, Alternative m)- => (Int, ListResult)- -> m (Either String ())-install' (_, ListResult {..}) = do- AppState { ghcupInfo = GHCupInfo { _ghcupDownloads = dls }} <- ask-- let run =- runResourceT- . runE- @'[ AlreadyInstalled- , ArchiveResult- , UnknownArchive- , FileDoesNotExistError- , CopyError- , NoDownload- , NotInstalled- , BuildFailed- , TagNotFound- , DigestError- , ContentLengthError- , GPGError- , DownloadFailed- , DirNotEmpty- , NoUpdate- , TarDirDoesNotExist- , FileAlreadyExistsError- , ProcessError- , ToolShadowed- , UninstallFailed- , MergeFileTreeError- , NoCompatiblePlatform- , GHCup.Errors.ParseError- , UnsupportedSetupCombo- , DistroNotFound- , NoCompatibleArch- ]-- run (do- ce <- liftIO $ fmap (either (const Nothing) Just) $- try @_ @SomeException $ getExecutablePath >>= canonicalizePath- dirs <- lift getDirs- case lTool of- GHC -> do- let vi = getVersionInfo (GHCTargetVersion lCross lVer) GHC dls- liftE $ installGHCBin (GHCTargetVersion lCross lVer) GHCupInternal False [] $> (vi, dirs, ce)- Cabal -> do- let vi = getVersionInfo (GHCTargetVersion lCross lVer) Cabal dls- liftE $ installCabalBin lVer GHCupInternal False $> (vi, dirs, ce)- GHCup -> do- let vi = snd <$> getLatest dls GHCup- liftE $ upgradeGHCup Nothing False False $> (vi, dirs, ce)- HLS -> do- let vi = getVersionInfo (GHCTargetVersion lCross lVer) HLS dls- liftE $ installHLSBin lVer GHCupInternal False $> (vi, dirs, ce)- Stack -> do- let vi = getVersionInfo (GHCTargetVersion lCross lVer) Stack dls- liftE $ installStackBin lVer GHCupInternal False $> (vi, dirs, ce)- )- >>= \case- VRight (vi, Dirs{..}, Just ce) -> do- forM_ (_viPostInstall =<< vi) $ \msg -> logInfo msg- case lTool of- GHCup -> do-#if !IS_WINDOWS- 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-#else- logInfo "Please restart 'ghcup' for the changes to take effect"-#endif- _ -> 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 $ prettyHFError e <> "\n"- <> "Also check the logs in ~/.ghcup/logs"---set' :: (MonadReader AppState m, MonadIO m, MonadThrow m, MonadFail m, MonadMask m, MonadUnliftIO m, Alternative m)- => (Int, ListResult)- -> m (Either String ())-set' input@(_, ListResult {..}) = do- settings <- liftIO $ readIORef settings'-- let run =- flip runReaderT settings- . runResourceT- . runE- @'[ AlreadyInstalled- , ArchiveResult- , UnknownArchive- , FileDoesNotExistError- , CopyError- , NoDownload- , NotInstalled- , BuildFailed- , TagNotFound- , DigestError- , ContentLengthError- , GPGError- , DownloadFailed- , DirNotEmpty- , NoUpdate- , TarDirDoesNotExist- , FileAlreadyExistsError- , ProcessError- , ToolShadowed- , UninstallFailed- , MergeFileTreeError- , NoCompatiblePlatform- , GHCup.Errors.ParseError- , UnsupportedSetupCombo- , DistroNotFound- , NoCompatibleArch- ]-- run (do- case lTool of- GHC -> liftE $ setGHC (GHCTargetVersion lCross lVer) SetGHCOnly Nothing $> ()- Cabal -> liftE $ setCabal lVer $> ()- HLS -> liftE $ setHLS lVer SetHLSOnly Nothing $> ()- Stack -> liftE $ setStack lVer $> ()- GHCup -> do- promptAnswer <- getUserPromptResponse "Switching GHCup versions is not supported.\nDo you want to install the latest version? [Y/N]: "- case promptAnswer of- PromptYes -> do- void $ liftE $ upgradeGHCup Nothing False False- PromptNo -> pure ()- )- >>= \case- VRight _ -> pure $ Right ()- VLeft e -> case e of- (V (NotInstalled tool _)) -> do- promptAnswer <- getUserPromptResponse userPrompt- case promptAnswer of- PromptYes -> do- res <- install' input- case res of- (Left err) -> pure $ Left err- (Right _) -> do- logInfo "Setting now..."- set' input-- PromptNo -> pure $ Left (prettyHFError e)- where- userPrompt = L.toStrict . B.toLazyText . B.fromString $- "This Version of "- <> show tool- <> " you are trying to set is not installed.\n"- <> "Would you like to install it first? [Y/N]: "-- _ -> pure $ Left (prettyHFError e)----del' :: (MonadReader AppState m, MonadIO m, MonadFail m, MonadMask m, MonadUnliftIO m)- => (Int, ListResult)- -> m (Either String ())-del' (_, ListResult {..}) = do- AppState { ghcupInfo = GHCupInfo { _ghcupDownloads = dls }} <- ask-- let run = runE @'[NotInstalled, UninstallFailed]-- run (do- let vi = getVersionInfo (GHCTargetVersion lCross lVer) lTool dls- case lTool of- GHC -> liftE $ rmGHCVer (GHCTargetVersion lCross lVer) $> vi- Cabal -> liftE $ rmCabalVer lVer $> vi- HLS -> liftE $ rmHLSVer lVer $> vi- Stack -> liftE $ rmStackVer lVer $> vi- GHCup -> pure Nothing- )- >>= \case- VRight vi -> do- when (lTool == GHC) $ logGHCPostRm (mkTVer lVer)- forM_ (_viPostRemove =<< vi) $ \msg ->- logInfo msg- pure $ Right ()- VLeft e -> pure $ Left (prettyHFError e)---changelog' :: (MonadReader AppState m, MonadIO m)- => (Int, ListResult)- -> m (Either String ())-changelog' (_, ListResult {..}) = do- AppState { pfreq, ghcupInfo = GHCupInfo { _ghcupDownloads = dls }} <- ask- case getChangeLog dls lTool (ToolVersion lVer) of- Nothing -> pure $ Left $- "Could not find ChangeLog for " <> prettyShow lTool <> ", version " <> T.unpack (prettyVer lVer)- Just uri -> do- case _rPlatform pfreq of- Darwin -> exec "open" [T.unpack $ decUTF8Safe $ serializeURIRef' uri] Nothing Nothing- Linux _ -> exec "xdg-open" [T.unpack $ decUTF8Safe $ serializeURIRef' uri] Nothing Nothing- FreeBSD -> exec "xdg-open" [T.unpack $ decUTF8Safe $ serializeURIRef' uri] Nothing Nothing- Windows -> do- let args = "start \"\" " ++ (T.unpack $ decUTF8Safe $ serializeURIRef' uri)- c <- liftIO $ system $ args- case c of- (ExitFailure xi) -> pure $ Left $ NonZeroExit xi "cmd.exe" [args]- ExitSuccess -> pure $ Right ()-- >>= \case- Right _ -> pure $ Right ()- Left e -> pure $ Left $ prettyHFError e---settings' :: IORef AppState-{-# NOINLINE settings' #-}-settings' = unsafePerformIO $ do- dirs <- getAllDirs- let loggerConfig = LoggerConfig { lcPrintDebug = False- , consoleOutter = \_ -> pure ()- , fileOutter = \_ -> pure ()- , fancyColors = True- }- newIORef $ AppState defaultSettings- dirs- defaultKeyBindings- (GHCupInfo mempty mempty Nothing)- (PlatformRequest A_64 Darwin Nothing)- loggerConfig---brickMain :: AppState- -> IO ()-brickMain s = do- writeIORef settings' s-- eAppData <- getAppData (Just $ ghcupInfo s)- case eAppData of- Right ad ->- Brick.defaultMain- (app (defaultAttributes (noColor $ settings s)) (dimAttributes (noColor $ settings s)))- (BrickState ad- defaultAppSettings- (constructList ad defaultAppSettings Nothing)- (keyBindings (s :: AppState))- Navigation-- )- $> ()- Left e -> do- flip runReaderT s $ logError $ "Error building app state: " <> T.pack (show e)- exitWith $ ExitFailure 2---defaultAppSettings :: BrickSettings-defaultAppSettings = BrickSettings { _showAllVersions = False}---getGHCupInfo :: IO (Either String GHCupInfo)-getGHCupInfo = do- settings <- readIORef settings'-- r <-- flip runReaderT settings- . runE @'[DigestError, ContentLengthError, GPGError, JSONError , DownloadFailed , FileDoesNotExistError, StackPlatformDetectError]- $ do- pfreq <- lift getPlatformReq- liftE $ getDownloadsF pfreq-- case r of- VRight a -> pure $ Right a- VLeft e -> pure $ Left (prettyHFError e)---getAppData :: Maybe GHCupInfo- -> IO (Either String BrickData)-getAppData mgi = runExceptT $ do- r <- ExceptT $ maybe getGHCupInfo (pure . Right) mgi- liftIO $ modifyIORef settings' (\s -> s { ghcupInfo = r })- settings <- liftIO $ readIORef settings'-- flip runReaderT settings $ do- lV <- listVersions Nothing [] False True (Nothing, Nothing)- pure $ BrickData (reverse lV)
app/ghcup/Main.hs view
@@ -10,8 +10,10 @@ module Main where +import GHCup.PlanJson+ #if defined(BRICK)-import BrickMain ( brickMain )+import GHCup.BrickMain (brickMain) #endif import qualified GHCup.GHC as GHC@@ -24,12 +26,12 @@ import GHCup.Types import GHCup.Types.Optics hiding ( toolRequirements ) import GHCup.Utils+import GHCup.Utils.Parsers (fromVersion) import GHCup.Prelude import GHCup.Prelude.Logger import GHCup.Prelude.String.QQ import GHCup.Version -import Cabal.Plan ( findPlanJson, SearchPlanJson(..) ) import Control.Concurrent import Control.Concurrent.Async import Control.Exception.Safe@@ -90,6 +92,7 @@ gpgSetting = fromMaybe (fromMaybe (Types.gpgSetting defaultSettings) uGPGSetting) optGpg platformOverride = optPlatform <|> (uPlatformOverride <|> Types.platformOverride defaultSettings) mirrors = fromMaybe (Types.mirrors defaultSettings) uMirrors+ defGHCConfOptions = fromMaybe (Types.defGHCConfOptions defaultSettings) uDefGHCConfOptions in (Settings {..}, keyBindings) #if defined(INTERNAL_DOWNLOADER) defaultDownloader = Internal@@ -111,11 +114,10 @@ } - plan_json :: String plan_json = $( do (fp, c) <- runIO (handleIO (\_ -> pure ("", "")) $ do- fp <- findPlanJson (ProjectRelativeToDir ".")+ fp <- findPlanJson "." c <- B.readFile fp (Just res) <- pure $ decodeStrict' @Value c pure (fp, T.unpack $ decUTF8Safe' $ encodePretty res))
data/config.yaml view
@@ -128,3 +128,12 @@ authority: host: "mirror.sjtu.edu.cn" +# Arguments to pass to the configure script of the prebuilt bindist.+#+# Do not pass '--prefix' here.+#+# GHCup by default passes '--disable-ld-override', so if you want to enable+# the vanilla way, which aggressively favors 'ld.gold' linker, add the following:+def-ghc-conf-options:+ - "--enable-ld-override"+
ghcup.cabal view
@@ -1,6 +1,6 @@ cabal-version: 2.4 name: ghcup-version: 0.1.22.0+version: 0.1.30.0 license: LGPL-3.0-only license-file: LICENSE copyright: Julian Ospald 2024@@ -70,8 +70,6 @@ , async ^>=2.2.3 , base >=4.12 && <5 , bytestring >=0.10 && <0.12- , cabal-install-parsers >=0.4.5- , cabal-plan ^>=0.7.2 , containers ^>=0.6 , deepseq ^>=1.4 , directory ^>=1.3.6.0@@ -80,6 +78,7 @@ , haskus-utils-variant ^>=3.3 , megaparsec >=8.0.0 && <9.3 , mtl ^>=2.2+ , optics ^>=0.4 , optparse-applicative >=0.15.1.0 && <0.18 , pretty ^>=1.1.3.1 , pretty-terminal ^>=0.1.0.0@@ -92,12 +91,13 @@ , temporary ^>=1.3 , text ^>=2.0 , time >=1.9.3 && <1.12+ , transformers ^>=0.5 , unordered-containers ^>=0.2 , uri-bytestring ^>=0.3.2.2 , utf8-string ^>=1.0 , vector >=0.12 && <0.14 , versions >=6.0.5 && <6.1- , yaml-streamly ^>=0.12.0+ , yaml ^>=0.11.0 if flag(tar) cpp-options: -DTAR@@ -109,9 +109,11 @@ build-depends: libarchive ^>=3.0.3.0 library+ import: app-common-depends exposed-modules: GHCup GHCup.Cabal+ GHCup.CabalConfig GHCup.Download GHCup.Download.Utils GHCup.Errors@@ -119,6 +121,7 @@ GHCup.HLS GHCup.List GHCup.Platform+ GHCup.PlanJson GHCup.Prelude GHCup.Prelude.File GHCup.Prelude.File.Search@@ -143,6 +146,7 @@ GHCup.Utils.Tar GHCup.Utils.Tar.Types GHCup.Utils.URI+ GHCup.Utils.Parsers GHCup.Version hs-source-dirs: lib@@ -177,9 +181,11 @@ , bytestring >=0.10 && <0.12 , bz2 ^>=1.0.1.1 , Cabal ^>=3.0.0.0 || ^>=3.2.0.0 || ^>=3.4.0.0 || ^>=3.6.0.0 || ^>=3.8.0.0 || ^>= 3.10.0.0+ , Cabal-syntax ^>=3.6.0.0 || ^>=3.8.0.0 || ^>= 3.10.0.0 || ^>= 3.12.0.0 , case-insensitive ^>=1.2.1.0 , casing ^>=0.1.4.1 , containers ^>=0.6+ , conduit ^>=1.3 , cryptohash-sha256 ^>=0.11.101.0 , deepseq ^>=1.4.4.0 , directory ^>=1.3.6.0@@ -192,8 +198,8 @@ , lzma-static ^>=5.2.5.3 , megaparsec >=8.0.0 && <9.3 , mtl ^>=2.2- , optics ^>=0.4 , os-release ^>=1.0.0+ , parsec , pretty ^>=1.1.3.1 , pretty-terminal ^>=0.1.0.0 , regex-posix ^>=0.96@@ -202,20 +208,19 @@ , safe ^>=0.3.18 , safe-exceptions ^>=0.1 , split ^>=0.2.3.4- , streamly ^>=0.8.2 , strict-base ^>=0.4 , template-haskell >=2.7 && <2.22 , temporary ^>=1.3 , text ^>=2.0 , time >=1.9.3 && <1.12- , transformers ^>=0.5 , unliftio-core ^>=0.2.0.1 , unordered-containers ^>=0.2.10.0 , uri-bytestring ^>=0.3.2.2+ , utf8-string ^>=1.0 , vector >=0.12 && <0.14 , versions >=6.0.5 && <6.1 , word8 ^>=0.1.3- , yaml-streamly ^>=0.12.0+ , yaml ^>=0.11.0 , zlib ^>=0.6.2.2 if flag(tar)@@ -257,7 +262,6 @@ exposed-modules: GHCup.Prelude.File.Posix.Traversals include-dirs: cbits- includes: dirutils.h install-includes: dirutils.h c-sources: cbits/dirutils.c build-depends:@@ -322,6 +326,55 @@ else build-depends: unix ^>=2.7 || ^>=2.8 +library ghcup-tui+ import: app-common-depends+ exposed-modules:+ GHCup.BrickMain+ GHCup.Brick.Widgets.Navigation+ GHCup.Brick.Widgets.Tutorial+ GHCup.Brick.Widgets.KeyInfo+ GHCup.Brick.Widgets.SectionList+ GHCup.Brick.Widgets.Menu+ GHCup.Brick.Widgets.Menus.Context+ GHCup.Brick.Widgets.Menus.AdvanceInstall+ GHCup.Brick.Widgets.Menus.CompileGHC+ GHCup.Brick.Widgets.Menus.CompileHLS+ GHCup.Brick.Actions+ GHCup.Brick.App+ GHCup.Brick.BrickState+ GHCup.Brick.Attributes+ GHCup.Brick.Common++ hs-source-dirs: lib-tui+ default-language: Haskell2010+ default-extensions:+ LambdaCase+ MultiWayIf+ NamedFieldPuns+ PackageImports+ RecordWildCards+ ScopedTypeVariables+ StrictData+ TupleSections++ ghc-options:+ -Wall -fwarn-tabs -fwarn-incomplete-uni-patterns+ -fwarn-incomplete-record-updates++ build-depends:+ , ghcup+ , brick ^>=2.1+ , vty ^>=6.0 || ^>=6.1 || ^>=6.2++ if !flag(tui)+ buildable: False++ if os(windows)+ cpp-options: -DIS_WINDOWS++ else+ build-depends: unix ^>=2.7 || ^>=2.8+ executable ghcup import: app-common-depends main-is: Main.hs@@ -351,12 +404,8 @@ if flag(tui) cpp-options: -DBRICK- other-modules: BrickMain build-depends:- , brick ^>=2.1- , transformers ^>=0.5- , vty ^>=6.0 || ^>=6.1 || ^>=6.2- , optics ^>=0.4+ , ghcup-tui if os(windows) cpp-options: -DIS_WINDOWS@@ -395,6 +444,7 @@ , base >=4.12 && <5 , bytestring >=0.10 && <0.12 , containers ^>=0.6+ , conduit ^>=1.3 , directory ^>=1.3.6.0 , filepath >=1.4.101.0 , generic-arbitrary >=0.1.0 && <0.2.1 || >=0.2.2 && <0.3@@ -403,7 +453,6 @@ , hspec-golden-aeson ^>=0.9 , QuickCheck ^>=2.14.1 , quickcheck-arbitrary-adt ^>=0.3.1.0- , streamly ^>=0.8.2 , text ^>=2.0 , time >=1.9.3 && <1.12 , uri-bytestring ^>=0.3.2.2
lib-opt/GHCup/OptParse.hs view
@@ -52,7 +52,7 @@ import GHCup.OptParse.Nuke import GHCup.Types-+import GHCup.Utils.Parsers (gpgParser, downloaderParser, keepOnParser, platformParser, parseUrlSource) #if !MIN_VERSION_base(4,13,0) import Control.Monad.Fail ( MonadFail ) #endif@@ -176,7 +176,7 @@ "GPG verification (default: none)" <> completer (listCompleter ["strict", "lax", "none"]) ))- <*> invertableSwitch "stack-setup" (Just 's') False (help "Use stack's setup info for discovering and installing GHC versions")+ <*> invertableSwitch "stack-setup" Nothing False (help "Use stack's setup info for discovering and installing GHC versions") <*> com
lib-opt/GHCup/OptParse/Common.hs view
@@ -11,22 +11,21 @@ import GHCup+import GHCup.CabalConfig import GHCup.Download-import GHCup.Errors import GHCup.Platform import GHCup.Types import GHCup.Types.Optics import GHCup.Utils+import qualified GHCup.Utils.Parsers as Parsers import GHCup.Prelude import GHCup.Prelude.Process import GHCup.Prelude.Logger-import GHCup.Prelude.MegaParsec import Control.DeepSeq import Control.Concurrent import Control.Concurrent.Async import Control.Exception.Safe-import Control.Monad.Identity (Identity(..)) #if !MIN_VERSION_base(4,13,0) import Control.Monad.Fail ( MonadFail ) #endif@@ -43,65 +42,25 @@ import Data.Char import Data.Either import Data.Functor-import Data.List ( nub, sort, sortBy, isPrefixOf, stripPrefix )+import Data.List ( nub, isPrefixOf, stripPrefix ) import Data.Maybe-import Data.Text ( Text )-import Data.Time.Calendar ( Day )-import Data.Time.Format ( parseTimeM, defaultTimeLocale ) import Data.Versions-import Data.Void import qualified Data.Vector as V import GHC.IO.Exception import Haskus.Utils.Variant.Excepts import Options.Applicative hiding ( style ) import Prelude hiding ( appendFile )-import Safe+import Safe (lastMay) import System.Process ( readProcess ) import System.FilePath import Text.HTML.TagSoup hiding ( Tag )-import URI.ByteString hiding (parseURI) -import qualified Data.ByteString.UTF8 as UTF8 import qualified Data.Map.Strict as M import qualified Data.Text as T-import qualified Data.Text.Lazy.Encoding as LE-import qualified Data.Text.Lazy as LT-import qualified Text.Megaparsec as MP import qualified System.FilePath.Posix as FP import GHCup.Version import Control.Exception (evaluate)-import qualified Cabal.Config as CC -- -------------- --[ Types ]--- ----------------- a superset of ToolVersion-data SetToolVersion = SetGHCVersion GHCTargetVersion- | SetToolVersion Version- | SetToolTag Tag- | SetToolDay Day- | SetRecommended- | SetNext- deriving (Eq, Show)--prettyToolVer :: ToolVersion -> String-prettyToolVer (GHCVersion v') = T.unpack $ tVerToText v'-prettyToolVer (ToolVersion v') = T.unpack $ prettyVer v'-prettyToolVer (ToolTag t) = show t-prettyToolVer (ToolDay day) = show day--toSetToolVer :: Maybe ToolVersion -> SetToolVersion-toSetToolVer (Just (GHCVersion v')) = SetGHCVersion v'-toSetToolVer (Just (ToolVersion v')) = SetToolVersion v'-toSetToolVer (Just (ToolTag t')) = SetToolTag t'-toSetToolVer (Just (ToolDay d')) = SetToolDay d'-toSetToolVer Nothing = SetRecommended---- -------------- --[ Parser ]-- --------------@@ -118,9 +77,9 @@ mv (Just HLS) = "HLS_VERSION|TAG|RELEASE_DATE" mv _ = "VERSION|TAG|RELEASE_DATE" - parser (Just GHC) = ghcVersionTagEither- parser Nothing = ghcVersionTagEither- parser _ = toolVersionTagEither+ parser (Just GHC) = Parsers.ghcVersionTagEither+ parser Nothing = Parsers.ghcVersionTagEither+ parser _ = Parsers.toolVersionTagEither versionParser' :: [ListCriteria] -> Maybe Tool -> Parser Version@@ -129,7 +88,7 @@ (metavar "VERSION" <> foldMap (completer . versionCompleter criteria) tool) ghcVersionArgument :: [ListCriteria] -> Maybe Tool -> Parser GHCTargetVersion-ghcVersionArgument criteria tool = argument (eitherReader ghcVersionEither)+ghcVersionArgument criteria tool = argument (eitherReader Parsers.ghcVersionEither) (metavar "VERSION" <> foldMap (completer . versionCompleter criteria) tool) @@ -171,170 +130,6 @@ - ---------------------- --[ Either Parser ]--- ------------------------platformParser :: String -> Either String PlatformRequest-platformParser s' = case MP.parse (platformP <* MP.eof) "" (T.pack s') of- Right r -> pure r- Left e -> Left $ errorBundlePretty e- where- archP :: MP.Parsec Void Text Architecture- archP = MP.try (MP.chunk "x86_64" $> A_64) <|> (MP.chunk "i386" $> A_32)- platformP :: MP.Parsec Void Text PlatformRequest- platformP = choice'- [ (`PlatformRequest` FreeBSD)- <$> (archP <* MP.chunk "-")- <*> ( MP.chunk "portbld"- *> ( MP.try (Just <$> verP (MP.chunk "-freebsd" <* MP.eof))- <|> pure Nothing- )- <* MP.chunk "-freebsd"- )- , (`PlatformRequest` Darwin)- <$> (archP <* MP.chunk "-")- <*> ( MP.chunk "apple"- *> ( MP.try (Just <$> verP (MP.chunk "-darwin" <* MP.eof))- <|> pure Nothing- )- <* MP.chunk "-darwin"- )- , (\a d mv -> PlatformRequest a (Linux d) mv)- <$> (archP <* MP.chunk "-")- <*> distroP- <*> ((MP.try (Just <$> verP (MP.chunk "-linux" <* MP.eof)) <|> pure Nothing- )- <* MP.chunk "-linux"- )- ]- distroP :: MP.Parsec Void Text LinuxDistro- distroP = choice' ((\d -> MP.chunk (T.pack $ distroToString d) $> d) <$> allDistros)---uriParser :: String -> Either String URI-uriParser = first show . parseURI . UTF8.fromString---absolutePathParser :: FilePath -> Either String FilePath-absolutePathParser f = case isValid f && isAbsolute f of- True -> Right $ normalise f- False -> Left "Please enter a valid absolute filepath."--isolateParser :: FilePath -> Either String FilePath-isolateParser f = case isValid f && isAbsolute f of- True -> Right $ normalise f- False -> Left "Please enter a valid filepath for isolate dir."---- this accepts cross prefix-ghcVersionTagEither :: String -> Either String ToolVersion-ghcVersionTagEither s' =- second ToolDay (dayParser s') <|> second ToolTag (tagEither s') <|> second GHCVersion (ghcVersionEither s')---- this ignores cross prefix-toolVersionTagEither :: String -> Either String ToolVersion-toolVersionTagEither s' =- second ToolDay (dayParser s') <|> second ToolTag (tagEither s') <|> second ToolVersion (toolVersionEither s')--tagEither :: String -> Either String Tag-tagEither s' = case fmap toLower s' of- "recommended" -> Right Recommended- "latest" -> Right Latest- "latest-prerelease" -> Right LatestPrerelease- "latest-nightly" -> Right LatestNightly- ('b':'a':'s':'e':'-':ver') -> case pvp (T.pack ver') of- Right x -> Right (Base x)- Left _ -> Left $ "Invalid PVP version for base " <> ver'- other -> Left $ "Unknown tag " <> other---ghcVersionEither :: String -> Either String GHCTargetVersion-ghcVersionEither =- first (const "Not a valid version") . MP.parse ghcTargetVerP "" . T.pack--toolVersionEither :: String -> Either String Version-toolVersionEither =- first (const "Not a valid version") . MP.parse (version' <* MP.eof) "" . T.pack---toolParser :: String -> Either String Tool-toolParser s' | t == T.pack "ghc" = Right GHC- | t == T.pack "cabal" = Right Cabal- | t == T.pack "hls" = Right HLS- | t == T.pack "stack" = Right Stack- | otherwise = Left ("Unknown tool: " <> s')- where t = T.toLower (T.pack s')--dayParser :: String -> Either String Day-dayParser s = maybe (Left $ "Could not parse \"" <> s <> "\". Expected format is: YYYY-MM-DD") Right- $ parseTimeM True defaultTimeLocale "%Y-%-m-%-d" s---criteriaParser :: String -> Either String ListCriteria-criteriaParser s' | t == T.pack "installed" = Right $ ListInstalled True- | t == T.pack "set" = Right $ ListSet True- | t == T.pack "available" = Right $ ListAvailable True- | t == T.pack "+installed" = Right $ ListInstalled True- | t == T.pack "+set" = Right $ ListSet True- | t == T.pack "+available" = Right $ ListAvailable True- | t == T.pack "-installed" = Right $ ListInstalled False- | t == T.pack "-set" = Right $ ListSet False- | t == T.pack "-available" = Right $ ListAvailable False- | otherwise = Left ("Unknown criteria: " <> s')- where t = T.toLower (T.pack s')----keepOnParser :: String -> Either String KeepDirs-keepOnParser s' | t == T.pack "always" = Right Always- | t == T.pack "errors" = Right Errors- | t == T.pack "never" = Right Never- | otherwise = Left ("Unknown keep value: " <> s')- where t = T.toLower (T.pack s')---downloaderParser :: String -> Either String Downloader-downloaderParser s' | t == T.pack "curl" = Right Curl- | t == T.pack "wget" = Right Wget-#if defined(INTERNAL_DOWNLOADER)- | t == T.pack "internal" = Right Internal-#endif- | otherwise = Left ("Unknown downloader value: " <> s')- where t = T.toLower (T.pack s')--gpgParser :: String -> Either String GPGSetting-gpgParser s' | t == T.pack "strict" = Right GPGStrict- | t == T.pack "lax" = Right GPGLax- | t == T.pack "none" = Right GPGNone- | otherwise = Left ("Unknown gpg setting value: " <> s')- where t = T.toLower (T.pack s')----overWriteVersionParser :: String -> Either String [VersionPattern]-overWriteVersionParser = first (const "Not a valid version pattern") . MP.parse (MP.many versionPattern <* MP.eof) "" . T.pack- where- versionPattern :: MP.Parsec Void Text VersionPattern- versionPattern = do- str' <- T.unpack <$> MP.takeWhileP Nothing (/= '%')- if str' /= mempty- then pure (S str')- else fmap (const CabalVer) v_cabal- <|> fmap (const GitBranchName) b_name- <|> fmap (const GitHashShort) s_hash- <|> fmap (const GitHashLong) l_hash- <|> fmap (const GitDescribe) g_desc- <|> ((\a b -> S (a : T.unpack b)) <$> MP.satisfy (const True) <*> MP.takeWhileP Nothing (== '%')) -- invalid pattern, e.g. "%k"- where- v_cabal = MP.chunk "%v"- b_name = MP.chunk "%b"- s_hash = MP.chunk "%h"- l_hash = MP.chunk "%H"- g_desc = MP.chunk "%g"--- ------------------ --[ Completers ]-- ------------------@@ -671,149 +466,6 @@ #endif ----- ------------------ --[ Utilities ]--- --------------------fromVersion :: ( HasLog env- , MonadFail m- , MonadReader env m- , HasGHCupInfo env- , HasDirs env- , MonadThrow m- , MonadIO m- , MonadCatch m- )- => Maybe ToolVersion- -> Tool- -> Excepts- '[ TagNotFound- , DayNotFound- , NextVerNotFound- , NoToolVersionSet- ] m (GHCTargetVersion, Maybe VersionInfo)-fromVersion tv = fromVersion' (toSetToolVer tv)--fromVersion' :: ( HasLog env- , MonadFail m- , MonadReader env m- , HasGHCupInfo env- , HasDirs env- , MonadThrow m- , MonadIO m- , MonadCatch m- )- => SetToolVersion- -> Tool- -> Excepts- '[ TagNotFound- , DayNotFound- , NextVerNotFound- , NoToolVersionSet- ] m (GHCTargetVersion, Maybe VersionInfo)-fromVersion' SetRecommended tool = do- GHCupInfo { _ghcupDownloads = dls } <- lift getGHCupInfo- second Just <$> getRecommended dls tool- ?? TagNotFound Recommended tool-fromVersion' (SetGHCVersion v) tool = do- GHCupInfo { _ghcupDownloads = dls } <- lift getGHCupInfo- let vi = getVersionInfo v tool dls- case pvp $ prettyVer (_tvVersion v) of -- need to be strict here- Left _ -> pure (v, vi)- Right pvpIn ->- lift (getLatestToolFor tool (_tvTarget v) pvpIn dls) >>= \case- Just (pvp_, vi', mt) -> do- v' <- lift $ pvpToVersion pvp_ ""- when (v' /= _tvVersion v) $ lift $ logWarn ("Assuming you meant version " <> prettyVer v')- pure (GHCTargetVersion mt v', Just vi')- Nothing -> pure (v, vi)-fromVersion' (SetToolVersion (mkTVer -> v)) tool = do- GHCupInfo { _ghcupDownloads = dls } <- lift getGHCupInfo- let vi = getVersionInfo v tool dls- case pvp $ prettyVer (_tvVersion v) of -- need to be strict here- Left _ -> pure (v, vi)- Right pvpIn ->- lift (getLatestToolFor tool (_tvTarget v) pvpIn dls) >>= \case- Just (pvp_, vi', mt) -> do- v' <- lift $ pvpToVersion pvp_ ""- when (v' /= _tvVersion v) $ lift $ logWarn ("Assuming you meant version " <> prettyVer v')- pure (GHCTargetVersion mt v', Just vi')- Nothing -> pure (v, vi)-fromVersion' (SetToolTag Latest) tool = do- GHCupInfo { _ghcupDownloads = dls } <- lift getGHCupInfo- bimap id Just <$> getLatest dls tool ?? TagNotFound Latest tool-fromVersion' (SetToolDay day) tool = do- GHCupInfo { _ghcupDownloads = dls } <- lift getGHCupInfo- bimap id Just <$> case getByReleaseDay dls tool day of- Left ad -> throwE $ DayNotFound day tool ad- Right v -> pure v-fromVersion' (SetToolTag LatestPrerelease) tool = do- GHCupInfo { _ghcupDownloads = dls } <- lift getGHCupInfo- bimap id Just <$> getLatestPrerelease dls tool ?? TagNotFound LatestPrerelease tool-fromVersion' (SetToolTag LatestNightly) tool = do- GHCupInfo { _ghcupDownloads = dls } <- lift getGHCupInfo- bimap id Just <$> getLatestNightly dls tool ?? TagNotFound LatestNightly tool-fromVersion' (SetToolTag Recommended) tool = do- GHCupInfo { _ghcupDownloads = dls } <- lift getGHCupInfo- bimap id Just <$> getRecommended dls tool ?? TagNotFound Recommended tool-fromVersion' (SetToolTag (Base pvp'')) GHC = do- GHCupInfo { _ghcupDownloads = dls } <- lift getGHCupInfo- bimap id Just <$> getLatestBaseVersion dls pvp'' ?? TagNotFound (Base pvp'') GHC-fromVersion' SetNext tool = do- GHCupInfo { _ghcupDownloads = dls } <- lift getGHCupInfo- next <- case tool of- GHC -> do- set <- fmap _tvVersion $ ghcSet Nothing !? NoToolVersionSet tool- ghcs <- rights <$> lift getInstalledGHCs- (headMay- . tail- . dropWhile (\GHCTargetVersion {..} -> _tvVersion /= set)- . cycle- . sortBy (\x y -> compare (_tvVersion x) (_tvVersion y))- . filter (\GHCTargetVersion {..} -> isNothing _tvTarget)- $ ghcs) ?? NoToolVersionSet tool- Cabal -> do- set <- cabalSet !? NoToolVersionSet tool- cabals <- rights <$> lift getInstalledCabals- (fmap (GHCTargetVersion Nothing)- . headMay- . tail- . dropWhile (/= set)- . cycle- . sort- $ cabals) ?? NoToolVersionSet tool- HLS -> do- set <- hlsSet !? NoToolVersionSet tool- hlses <- rights <$> lift getInstalledHLSs- (fmap (GHCTargetVersion Nothing)- . headMay- . tail- . dropWhile (/= set)- . cycle- . sort- $ hlses) ?? NoToolVersionSet tool- Stack -> do- set <- stackSet !? NoToolVersionSet tool- stacks <- rights <$> lift getInstalledStacks- (fmap (GHCTargetVersion Nothing)- . headMay- . tail- . dropWhile (/= set)- . cycle- . sort- $ stacks) ?? NoToolVersionSet tool- GHCup -> fail "GHCup cannot be set"- let vi = getVersionInfo next tool dls- pure (next, vi)-fromVersion' (SetToolTag t') tool =- throwE $ TagNotFound t' tool-- checkForUpdates :: ( MonadReader env m , HasGHCupInfo env , HasDirs env@@ -844,23 +496,9 @@ where forMM a f = fmap join $ forM a f - logGHCPostRm :: (MonadReader env m, HasLog env, MonadIO m) => GHCTargetVersion -> m () logGHCPostRm ghcVer = do cabalStore <- liftIO $ handleIO (\_ -> if isWindows then pure "C:\\cabal\\store" else pure "~/.cabal/store or ~/.local/state/cabal/store")- (runIdentity . CC.cfgStoreDir <$> CC.readConfig)+ getStoreDir let storeGhcDir = cabalStore </> ("ghc-" <> T.unpack (prettyVer $ _tvVersion ghcVer)) logInfo $ T.pack $ "After removing GHC you might also want to clean up your cabal store at: " <> storeGhcDir--parseUrlSource :: String -> Either String URLSource-parseUrlSource "GHCupURL" = pure GHCupURL-parseUrlSource "StackSetupURL" = pure StackSetupURL-parseUrlSource s' = (eitherDecode . LE.encodeUtf8 . LT.pack $ s')- <|> (fmap (OwnSource . (:[]) . Right) . first show . parseURI .UTF8.fromString $ s')--parseNewUrlSource :: String -> Either String NewURLSource-parseNewUrlSource "GHCupURL" = pure NewGHCupURL-parseNewUrlSource "StackSetupURL" = pure NewStackSetupURL-parseNewUrlSource s' = (eitherDecode . LE.encodeUtf8 . LT.pack $ s')- <|> (fmap NewURI . first show . parseURI .UTF8.fromString $ s')-
lib-opt/GHCup/OptParse/Compile.hs view
@@ -18,6 +18,7 @@ import GHCup.Types import GHCup.Types.Optics import GHCup.Utils+import GHCup.Utils.Parsers (fromVersion, uriParser, ghcVersionTagEither, isolateParser, overWriteVersionParser) import GHCup.Prelude.Logger import GHCup.Prelude.String.QQ import GHCup.OptParse.Common@@ -68,6 +69,7 @@ data GHCCompileOptions = GHCCompileOptions { targetGhc :: GHC.GHCVer , bootstrapGhc :: Either Version FilePath+ , hadrianGhc :: Maybe (Either Version FilePath) , jobs :: Maybe Int , buildConfig :: Maybe FilePath , patches :: Maybe (Either FilePath [URI])@@ -163,7 +165,7 @@ ghcCompileOpts :: Parser GHCCompileOptions ghcCompileOpts =- GHCCompileOptions+ (\targetGhc bootstrapGhc hadrianGhc jobs patches crossTarget addConfArgs setCompile overwriteVer buildFlavour (buildSystem, buildConfig) isolateDir -> GHCCompileOptions {..}) <$> ((GHC.SourceDist <$> option (eitherReader (first (const "Not a valid version") . version . T.pack)@@ -207,6 +209,18 @@ "The GHC version (or full path) to bootstrap with (must be installed)" <> (completer $ versionCompleter [] GHC) )+ <*> optional (option+ (eitherReader+ (\x ->+ (bimap (const "Not a valid version") Left . version . T.pack $ x) <|> (if isPathSeparator (head x) then pure $ Right x else Left "Not an absolute Path")+ )+ )+ ( long "hadrian-ghc"+ <> metavar "HADRIAN_GHC"+ <> help+ "The GHC version (or full path) to GHC that will be used to compile hadrian (must be installed)"+ <> (completer $ versionCompleter [] GHC)+ )) <*> optional (option (eitherReader (readEither @Int))@@ -215,14 +229,6 @@ <> (completer $ listCompleter $ fmap show ([1..12] :: [Int])) ) )- <*> optional- (option- str- (short 'c' <> long "config" <> metavar "CONFIG" <> help- "Absolute path to build config file"- <> completer (bashCompleter "file")- )- ) <*> (optional ( (fmap Right $ many $ option@@ -274,13 +280,29 @@ ) ) <*> (- (\b -> if b then Just Hadrian else Nothing) <$> switch+ (\b -> (, Nothing) $ if b then Just Hadrian else Nothing) <$> switch (long "hadrian" <> help "Use the hadrian build system instead of make. Tries to detect by default." ) <|>- (\b -> if b then Just Make else Nothing) <$> switch+ ((\b c -> case (b, c) of+ -- --make specified+ (True, _) -> (Just Make, c)+ -- only --config specified... assume make+ (False, Just _) -> (Just Make, c)+ -- otherwise fall back to runtime detection of build+ -- system+ (False, _) -> (Nothing, c)) <$> switch (long "make" <> help "Use the make build system instead of hadrian. Tries to detect by default." )+ <*> optional+ (option+ str+ (short 'c' <> long "config" <> metavar "CONFIG" <> help+ "Absolute path to build config file (implies make)"+ <> completer (bashCompleter "file")+ )+ )+ ) ) <*> optional (option@@ -599,6 +621,7 @@ crossTarget overwriteVer bootstrapGhc+ hadrianGhc jobs buildConfig patches
lib-opt/GHCup/OptParse/Config.hs view
@@ -14,6 +14,7 @@ import GHCup.Errors import GHCup.Types import GHCup.Utils+import GHCup.Utils.Parsers (parseNewUrlSource) import GHCup.Prelude import GHCup.Prelude.Logger import GHCup.Prelude.String.QQ@@ -133,7 +134,8 @@ gpgSetting' = uGPGSetting usl <|> uGPGSetting usr platformOverride' = uPlatformOverride usl <|> uPlatformOverride usr mirrors' = uMirrors usl <|> uMirrors usr- in UserSettings cache' metaCache' metaMode' noVerify' verbose' keepDirs' downloader' (updateKeyBindings (uKeyBindings usl) (uKeyBindings usr)) urlSource' noNetwork' gpgSetting' platformOverride' mirrors'+ defGHCconfOptions' = uDefGHCConfOptions usl <|> uDefGHCConfOptions usr+ in UserSettings cache' metaCache' metaMode' noVerify' verbose' keepDirs' downloader' (updateKeyBindings (uKeyBindings usl) (uKeyBindings usr)) urlSource' noNetwork' gpgSetting' platformOverride' mirrors' defGHCconfOptions' where updateKeyBindings :: Maybe UserKeyBindings -> Maybe UserKeyBindings -> Maybe UserKeyBindings updateKeyBindings Nothing Nothing = Nothing
lib-opt/GHCup/OptParse/Install.hs view
@@ -20,6 +20,7 @@ import GHCup.Errors import GHCup.Types import GHCup.Utils.Dirs+import GHCup.Utils.Parsers (fromVersion, isolateParser, uriParser) import GHCup.Prelude import GHCup.Prelude.Logger import GHCup.Prelude.String.QQ
lib-opt/GHCup/OptParse/List.hs view
@@ -14,6 +14,7 @@ import GHCup import GHCup.Prelude import GHCup.Types+import GHCup.Utils.Parsers (dayParser, toolParser, criteriaParser) import GHCup.OptParse.Common import GHCup.Prelude.String.QQ
lib-opt/GHCup/OptParse/Prefetch.hs view
@@ -14,6 +14,7 @@ import GHCup import GHCup.Errors import GHCup.Types+import GHCup.Utils.Parsers (fromVersion) import GHCup.Types.Optics import GHCup.Prelude.File import GHCup.Prelude.Logger
lib-opt/GHCup/OptParse/Run.hs view
@@ -11,6 +11,7 @@ import GHCup import GHCup.Utils+import GHCup.Utils.Parsers (fromVersion, ghcVersionTagEither, isolateParser, toolVersionTagEither) import GHCup.OptParse.Common import GHCup.Errors import GHCup.Types
lib-opt/GHCup/OptParse/Set.hs view
@@ -17,6 +17,7 @@ import GHCup import GHCup.Errors import GHCup.Types+import GHCup.Utils.Parsers (SetToolVersion(..), tagEither, ghcVersionEither, toolVersionEither, fromVersion') import GHCup.Prelude.Logger import GHCup.Prelude.String.QQ
lib-opt/GHCup/OptParse/Test.hs view
@@ -20,6 +20,7 @@ import GHCup.Errors import GHCup.Types import GHCup.Utils.Dirs+import GHCup.Utils.Parsers (fromVersion, uriParser) import GHCup.Prelude.Logger import GHCup.Prelude.String.QQ
lib-opt/GHCup/OptParse/Whereis.hs view
@@ -18,6 +18,7 @@ import GHCup.OptParse.Common import GHCup.Types import GHCup.Utils+import GHCup.Utils.Parsers (fromVersion) import GHCup.Prelude.Logger import GHCup.Prelude.String.QQ
+ lib-tui/GHCup/Brick/Actions.hs view
@@ -0,0 +1,738 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE RankNTypes #-}+{-# OPTIONS_GHC -Wno-unused-record-wildcards #-}+{-# OPTIONS_GHC -Wno-unused-matches #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}++module GHCup.Brick.Actions where++import GHCup+import GHCup.CabalConfig+import GHCup.Download+import GHCup.Errors+import GHCup.Types.Optics ( getDirs, getPlatformReq, HasLog )+import GHCup.Types hiding ( LeanAppState(..) )+import GHCup.Utils+import GHCup.Prelude ( decUTF8Safe, runBothE' )+import GHCup.Prelude.Logger+import GHCup.Prelude.Process+import GHCup.Prompts+import GHCup.Brick.Common (BrickData(..), BrickSettings(..), Name(..), Mode(..))+import qualified GHCup.Brick.Common as Common+import GHCup.Brick.BrickState+import GHCup.Brick.Widgets.SectionList+import qualified GHCup.Brick.Widgets.Menus.Context as ContextMenu+import GHCup.Brick.Widgets.Navigation (BrickInternalState)+import qualified GHCup.Brick.Widgets.Menus.AdvanceInstall as AdvanceInstall+import qualified GHCup.Brick.Widgets.Menus.CompileGHC as CompileGHC++import qualified Brick+import qualified Brick.Widgets.List as L+import qualified Brick.Focus as F+import Control.Applicative+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+import Data.Bool+import Data.Functor+import Data.Function ( (&), on)+import Data.List+import Data.Maybe+import Data.IORef (IORef, readIORef, newIORef, modifyIORef)+import Data.Versions hiding (Lens')+import Haskus.Utils.Variant.Excepts+import Prelude hiding ( appendFile )+import System.Exit+import System.IO.Unsafe+import System.Process ( system )+import Text.PrettyPrint.HughesPJClass ( prettyShow )+import URI.ByteString++import qualified Data.Text as T+import qualified Data.Text.Lazy.Builder as B+import qualified Data.Text.Lazy as L+import qualified Graphics.Vty as Vty+import qualified Data.Vector as V+import System.Environment (getExecutablePath)+#if !IS_WINDOWS+import GHCup.Prelude.File+import qualified System.Posix.Process as SPP+#endif++import System.FilePath++import Optics.State (use)+import Optics.State.Operators ( (.=))+import Optics.Operators ((.~),(%~))+import Optics.Getter (view)+import Optics.Optic ((%))+import Optics ((^.), to)+import qualified GHCup.Brick.Widgets.Menus.CompileHLS as CompileHLS+import Control.Concurrent (threadDelay)+import qualified GHCup.GHC as GHC+import qualified GHCup.Utils.Parsers as Utils+import qualified GHCup.HLS as HLS++++{- Core Logic.++This module defines the IO actions we can execute within the Brick App:+ - Install+ - Set+ - UnInstall+ - Launch the Changelog++-}++-- | Update app data and list internal state based on new evidence.+-- This synchronises @BrickInternalState@ with @BrickData@+-- and @BrickSettings@.+updateList :: BrickData -> BrickState -> BrickState+updateList appD bst =+ let newInternalState = constructList appD (bst ^. appSettings) (Just (bst ^. appState))+ in bst+ & appState .~ newInternalState+ & appData .~ appD+ & mode .~ Navigation++constructList :: BrickData+ -> BrickSettings+ -> Maybe BrickInternalState+ -> BrickInternalState+constructList appD settings =+ replaceLR (filterVisible (_showAllVersions settings))+ (_lr appD)++-- | Focus on the tool section and the predicate which matches. If no result matches, focus on index 0+selectBy :: Tool -> (ListResult -> Bool) -> BrickInternalState -> BrickInternalState+selectBy tool predicate internal_state =+ let new_focus = F.focusSetCurrent (Singular tool) (view sectionListFocusRingL internal_state)+ tool_lens = sectionL (Singular tool)+ in internal_state+ & sectionListFocusRingL .~ new_focus+ & tool_lens %~ L.listMoveTo 0 -- We move to 0 first+ & tool_lens %~ L.listFindBy predicate -- The lookup by the predicate.++-- | Select the latests GHC tool+selectLatest :: BrickInternalState -> BrickInternalState+selectLatest = selectBy GHC (elem Latest . lTag)+++-- | Replace the @appState@ or construct it based on a filter function+-- and a new @[ListResult]@ evidence.+-- When passed an existing @appState@, tries to keep the selected element.+replaceLR :: (ListResult -> Bool)+ -> [ListResult]+ -> Maybe BrickInternalState+ -> BrickInternalState+replaceLR filterF list_result s =+ let oldElem = s >>= sectionListSelectedElement -- Maybe (Int, e)+ newVec = [(Singular $ lTool (head g), V.fromList g) | g <- groupBy ((==) `on` lTool ) (filter filterF list_result)]+ newSectionList = sectionList AllTools newVec 1+ in case oldElem of+ Just (_, el) -> selectBy (lTool el) (toolEqual el) newSectionList+ Nothing -> selectLatest newSectionList+ where+ toolEqual e1 e2 =+ lTool e1 == lTool e2 && lVer e1 == lVer e2 && lCross e1 == lCross e2+++filterVisible :: Bool -> ListResult -> Bool+filterVisible v e | lInstalled e = True+ | v+ , Nightly `notElem` lTag e = True+ | not v+ , Old `notElem` lTag e+ , Nightly `notElem` lTag e = True+ | otherwise = (Old `notElem` lTag e) &&+ (Nightly `notElem` lTag e)++-- | Suspend the current UI and run an IO action in terminal. If the+-- IO action returns a Left value, then it's thrown as userError.+withIOAction :: (Ord n, Eq n)+ => ( (Int, ListResult) -> ReaderT AppState IO (Either String a))+ -> Brick.EventM n BrickState ()+withIOAction action = do+ as <- Brick.get+ case sectionListSelectedElement (view appState as) of+ Nothing -> pure ()+ Just (curr_ix, e) -> do+ Brick.suspendAndResume $ do+ settings <- readIORef settings'+ flip runReaderT settings $ action (curr_ix, e) >>= \case+ Left err -> liftIO $ putStrLn ("Error: " <> err)+ Right _ -> liftIO $ putStrLn "Success"+ getAppData Nothing >>= \case+ Right data' -> do+ putStrLn "Press enter to continue"+ _ <- getLine+ pure (updateList data' as)+ Left err -> throwIO $ userError err++installWithOptions :: (MonadReader AppState m, MonadIO m, MonadThrow m, MonadFail m, MonadMask m, MonadUnliftIO m, Alternative m)+ => AdvanceInstall.InstallOptions+ -> (Int, ListResult)+ -> m (Either String ())+installWithOptions opts (_, ListResult {..}) = do+ AppState { ghcupInfo = GHCupInfo { _ghcupDownloads = dls }} <- ask+ let+ misolated = opts ^. AdvanceInstall.isolateDirL+ shouldIsolate = maybe GHCupInternal IsolateDir (opts ^. AdvanceInstall.isolateDirL)+ shouldForce = opts ^. AdvanceInstall.forceInstallL+ shouldSet = opts ^. AdvanceInstall.instSetL+ extraArgs = opts ^. AdvanceInstall.addConfArgsL+ v = GHCTargetVersion lCross lVer+ let run =+ runResourceT+ . runE+ @'[ AlreadyInstalled+ , ArchiveResult+ , UnknownArchive+ , FileDoesNotExistError+ , CopyError+ , NoDownload+ , NotInstalled+ , BuildFailed+ , TagNotFound+ , DigestError+ , ContentLengthError+ , GPGError+ , DownloadFailed+ , DirNotEmpty+ , NoUpdate+ , TarDirDoesNotExist+ , FileAlreadyExistsError+ , ProcessError+ , ToolShadowed+ , UninstallFailed+ , MergeFileTreeError+ , NoCompatiblePlatform+ , GHCup.Errors.ParseError+ , UnsupportedSetupCombo+ , DistroNotFound+ , NoCompatibleArch+ , InstallSetError+ ]++ run (do+ ce <- liftIO $ fmap (either (const Nothing) Just) $+ try @_ @SomeException $ getExecutablePath >>= canonicalizePath+ dirs <- lift getDirs+ case lTool of+ GHC -> do+ let vi = getVersionInfo v GHC dls+ case opts ^. AdvanceInstall.instBindistL of+ Nothing -> do+ liftE $+ runBothE'+ (installGHCBin v shouldIsolate shouldForce extraArgs)+ (when (shouldSet && isNothing misolated) (liftE $ void $ setGHC v SetGHCOnly Nothing))+ pure (vi, dirs, ce)+ Just uri -> do+ liftE $+ runBothE'+ (installGHCBindist+ (DownloadInfo uri (Just $ RegexDir "ghc-.*") "" Nothing Nothing)+ v+ shouldIsolate+ shouldForce+ extraArgs)+ (when (shouldSet && isNothing misolated) (liftE $ void $ setGHC v SetGHCOnly Nothing))+ pure (vi, dirs, ce)++ Cabal -> do+ let vi = getVersionInfo v Cabal dls+ case opts ^. AdvanceInstall.instBindistL of+ Nothing -> do+ liftE $+ runBothE'+ (installCabalBin lVer shouldIsolate shouldForce)+ (when (shouldSet && isNothing misolated) (liftE $ void $ setCabal lVer))+ pure (vi, dirs, ce)+ Just uri -> do+ liftE $+ runBothE'+ (installCabalBindist (DownloadInfo uri Nothing "" Nothing Nothing) lVer shouldIsolate shouldForce)+ (when (shouldSet && isNothing misolated) (liftE $ void $ setCabal lVer))+ pure (vi, dirs, ce)++ GHCup -> do+ let vi = snd <$> getLatest dls GHCup+ liftE $ upgradeGHCup Nothing False False $> (vi, dirs, ce)+ HLS -> do+ let vi = getVersionInfo v HLS dls+ case opts ^. AdvanceInstall.instBindistL of+ Nothing -> do+ liftE $+ runBothE'+ (installHLSBin lVer shouldIsolate shouldForce)+ (when (shouldSet && isNothing misolated) (liftE $ void $ setHLS lVer SetHLSOnly Nothing))+ pure (vi, dirs, ce)+ Just uri -> do+ liftE $+ runBothE'+ (installHLSBindist+ (DownloadInfo uri (if isWindows then Nothing else Just (RegexDir "haskell-language-server-*")) "" Nothing Nothing)+ lVer+ shouldIsolate+ shouldForce)+ (when (shouldSet && isNothing misolated) (liftE $ void $ setHLS lVer SetHLSOnly Nothing))+ pure (vi, dirs, ce)++ Stack -> do+ let vi = getVersionInfo v Stack dls+ case opts ^. AdvanceInstall.instBindistL of+ Nothing -> do+ liftE $+ runBothE'+ (installStackBin lVer shouldIsolate shouldForce)+ (when (shouldSet && isNothing misolated) (liftE $ void $ setStack lVer))+ pure (vi, dirs, ce)+ Just uri -> do+ liftE $+ runBothE'+ (installStackBindist (DownloadInfo uri Nothing "" Nothing Nothing) lVer shouldIsolate shouldForce)+ (when (shouldSet && isNothing misolated) (liftE $ void $ setStack lVer))+ pure (vi, dirs, ce)++ )+ >>= \case+ VRight (vi, Dirs{..}, Just ce) -> do+ forM_ (_viPostInstall =<< vi) $ \msg -> logInfo msg+ case lTool of+ GHCup -> do+#if !IS_WINDOWS+ 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+#else+ logInfo "Please restart 'ghcup' for the changes to take effect"+#endif+ _ -> 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 $ prettyHFError e <> "\n"+ <> "Also check the logs in ~/.ghcup/logs"++install' :: (MonadReader AppState m, MonadIO m, MonadThrow m, MonadFail m, MonadMask m, MonadUnliftIO m, Alternative m)+ => (Int, ListResult) -> m (Either String ())+install' = installWithOptions (AdvanceInstall.InstallOptions Nothing False Nothing False [])++set' :: (MonadReader AppState m, MonadIO m, MonadThrow m, MonadFail m, MonadMask m, MonadUnliftIO m, Alternative m)+ => (Int, ListResult)+ -> m (Either String ())+set' input@(_, ListResult {..}) = do+ settings <- liftIO $ readIORef settings'++ let run =+ flip runReaderT settings+ . runResourceT+ . runE+ @'[ AlreadyInstalled+ , ArchiveResult+ , UnknownArchive+ , FileDoesNotExistError+ , CopyError+ , NoDownload+ , NotInstalled+ , BuildFailed+ , TagNotFound+ , DigestError+ , ContentLengthError+ , GPGError+ , DownloadFailed+ , DirNotEmpty+ , NoUpdate+ , TarDirDoesNotExist+ , FileAlreadyExistsError+ , ProcessError+ , ToolShadowed+ , UninstallFailed+ , MergeFileTreeError+ , NoCompatiblePlatform+ , GHCup.Errors.ParseError+ , UnsupportedSetupCombo+ , DistroNotFound+ , NoCompatibleArch+ ]++ run (do+ case lTool of+ GHC -> liftE $ setGHC (GHCTargetVersion lCross lVer) SetGHCOnly Nothing $> ()+ Cabal -> liftE $ setCabal lVer $> ()+ HLS -> liftE $ setHLS lVer SetHLSOnly Nothing $> ()+ Stack -> liftE $ setStack lVer $> ()+ GHCup -> do+ promptAnswer <- getUserPromptResponse "Switching GHCup versions is not supported.\nDo you want to install the latest version? [Y/N]: "+ case promptAnswer of+ PromptYes -> do+ void $ liftE $ upgradeGHCup Nothing False False+ PromptNo -> pure ()+ )+ >>= \case+ VRight _ -> pure $ Right ()+ VLeft e -> case e of+ (V (NotInstalled tool _)) -> do+ promptAnswer <- getUserPromptResponse userPrompt+ case promptAnswer of+ PromptYes -> do+ res <- install' input+ case res of+ (Left err) -> pure $ Left err+ (Right _) -> do+ logInfo "Setting now..."+ set' input++ PromptNo -> pure $ Left (prettyHFError e)+ where+ userPrompt = L.toStrict . B.toLazyText . B.fromString $+ "This Version of "+ <> show tool+ <> " you are trying to set is not installed.\n"+ <> "Would you like to install it first? [Y/N]: "++ _ -> pure $ Left (prettyHFError e)++logGHCPostRm :: (MonadReader env m, HasLog env, MonadIO m) => GHCTargetVersion -> m ()+logGHCPostRm ghcVer = do+ cabalStore <- liftIO $ handleIO (\_ -> if isWindows then pure "C:\\cabal\\store" else pure "~/.cabal/store or ~/.local/state/cabal/store")+ getStoreDir+ let storeGhcDir = cabalStore </> ("ghc-" <> T.unpack (prettyVer $ _tvVersion ghcVer))+ logInfo $ T.pack $ "After removing GHC you might also want to clean up your cabal store at: " <> storeGhcDir+++del' :: (MonadReader AppState m, MonadIO m, MonadFail m, MonadMask m, MonadUnliftIO m)+ => (Int, ListResult)+ -> m (Either String ())+del' (_, ListResult {..}) = do+ AppState { ghcupInfo = GHCupInfo { _ghcupDownloads = dls }} <- ask++ let run = runE @'[NotInstalled, UninstallFailed]++ run (do+ let vi = getVersionInfo (GHCTargetVersion lCross lVer) lTool dls+ case lTool of+ GHC -> liftE $ rmGHCVer (GHCTargetVersion lCross lVer) $> vi+ Cabal -> liftE $ rmCabalVer lVer $> vi+ HLS -> liftE $ rmHLSVer lVer $> vi+ Stack -> liftE $ rmStackVer lVer $> vi+ GHCup -> pure Nothing+ )+ >>= \case+ VRight vi -> do+ when (lTool == GHC) $ logGHCPostRm (mkTVer lVer)+ forM_ (_viPostRemove =<< vi) $ \msg ->+ logInfo msg+ pure $ Right ()+ VLeft e -> pure $ Left (prettyHFError e)+++changelog' :: (MonadReader AppState m, MonadIO m)+ => (Int, ListResult)+ -> m (Either String ())+changelog' (_, ListResult {..}) = do+ AppState { pfreq, ghcupInfo = GHCupInfo { _ghcupDownloads = dls }} <- ask+ case getChangeLog dls lTool (ToolVersion lVer) of+ Nothing -> pure $ Left $+ "Could not find ChangeLog for " <> prettyShow lTool <> ", version " <> T.unpack (prettyVer lVer)+ Just uri -> do+ case _rPlatform pfreq of+ Darwin -> exec "open" [T.unpack $ decUTF8Safe $ serializeURIRef' uri] Nothing Nothing+ Linux _ -> exec "xdg-open" [T.unpack $ decUTF8Safe $ serializeURIRef' uri] Nothing Nothing+ FreeBSD -> exec "xdg-open" [T.unpack $ decUTF8Safe $ serializeURIRef' uri] Nothing Nothing+ Windows -> do+ let args = "start \"\" " ++ (T.unpack $ decUTF8Safe $ serializeURIRef' uri)+ c <- liftIO $ system $ args+ case c of+ (ExitFailure xi) -> pure $ Left $ NonZeroExit xi "cmd.exe" [args]+ ExitSuccess -> pure $ Right ()++ >>= \case+ Right _ -> pure $ Right ()+ Left e -> pure $ Left $ prettyHFError e++compileGHC :: (MonadReader AppState m, MonadIO m, MonadThrow m, MonadFail m, MonadMask m, MonadUnliftIO m, Alternative m)+ => CompileGHC.CompileGHCOptions -> (Int, ListResult) -> m (Either String ())+compileGHC compopts (_, lr@ListResult{lTool = GHC, ..}) = do+ appstate <- ask+ let run =+ runResourceT+ . runE @'[ AlreadyInstalled+ , BuildFailed+ , DigestError+ , ContentLengthError+ , GPGError+ , DownloadFailed+ , GHCupSetError+ , NoDownload+ , NotFoundInPATH+ , PatchFailed+ , UnknownArchive+ , TarDirDoesNotExist+ , NotInstalled+ , DirNotEmpty+ , ArchiveResult+ , FileDoesNotExistError+ , HadrianNotFound+ , InvalidBuildConfig+ , ProcessError+ , CopyError+ , BuildFailed+ , UninstallFailed+ , MergeFileTreeError+ ]+ compileResult <- run (do+ AppState { ghcupInfo = GHCupInfo { _ghcupDownloads = dls }} <- ask+ ghcVer <- case compopts ^. CompileGHC.gitRef of+ Just ref -> pure (GHC.GitDist (GitBranch ref Nothing))+ Nothing -> do+ -- Compile the version user is pointing to in the tui+ let vi = getVersionInfo (mkTVer lVer) GHC dls+ forM_ (_viPreCompile =<< vi) $ \msg -> do+ logInfo msg+ logInfo+ "...waiting for 5 seconds, you can still abort..."+ liftIO $ threadDelay 5000000 -- for compilation, give the user a sec to intervene+ pure (GHC.SourceDist lVer)++ targetVer <- liftE $ GHCup.compileGHC+ ghcVer+ (compopts ^. CompileGHC.crossTarget)+ (compopts ^. CompileGHC.overwriteVer)+ (compopts ^. CompileGHC.bootstrapGhc)+ (compopts ^. CompileGHC.hadrianGhc)+ (compopts ^. CompileGHC.jobs)+ (compopts ^. CompileGHC.buildConfig)+ (compopts ^. CompileGHC.patches)+ (compopts ^. CompileGHC.addConfArgs)+ (compopts ^. CompileGHC.buildFlavour)+ (compopts ^. CompileGHC.buildSystem)+ (maybe GHCupInternal IsolateDir $ compopts ^. CompileGHC.isolateDir)+ AppState { ghcupInfo = GHCupInfo { _ghcupDownloads = dls2 }} <- ask+ let vi2 = getVersionInfo targetVer GHC dls2+ when+ (compopts ^. CompileGHC.setCompile)+ (liftE . void $ GHCup.setGHC targetVer SetGHCOnly Nothing)+ pure (vi2, targetVer)+ )+ case compileResult of+ VRight (vi, tv) -> do+ logInfo "GHC successfully compiled and installed"+ forM_ (_viPostInstall =<< vi) $ \msg -> logInfo msg+ liftIO $ putStr (T.unpack $ tVerToText tv)+ pure $ Right ()+ VLeft (V (AlreadyInstalled _ v)) -> do+ pure $ Left $+ "GHC ver " <> T.unpack (prettyVer v) <> " already installed, remove it first to reinstall"+ VLeft (V (DirNotEmpty fp)) -> do+ pure $ Left $+ "Install directory " <> fp <> " is not empty."+ VLeft err@(V (BuildFailed tmpdir _)) -> pure $ Left $+ case keepDirs (appstate & settings) of+ Never -> prettyHFError err+ _ -> prettyHFError err <> "\n"+ <> "Check the logs at " <> (fromGHCupPath $ appstate & dirs & logsDir)+ <> " and the build directory "+ <> tmpdir <> " for more clues." <> "\n"+ <> "Make sure to clean up " <> tmpdir <> " afterwards."+ VLeft e -> do+ pure $ Left $ prettyHFError e+-- This is the case when the tool is not GHC... which should be impossible but,+-- it exhaustes pattern matches+compileGHC _ (_, ListResult{lTool = _}) = pure (Right ())+++compileHLS :: (MonadReader AppState m, MonadIO m, MonadThrow m, MonadFail m, MonadMask m, MonadUnliftIO m, Alternative m)+ => CompileHLS.CompileHLSOptions -> (Int, ListResult) -> m (Either String ())+compileHLS compopts (_, lr@ListResult{lTool = HLS, ..}) = do+ appstate <- ask+ let run =+ runResourceT+ . runE @'[ AlreadyInstalled+ , BuildFailed+ , DigestError+ , ContentLengthError+ , GPGError+ , DownloadFailed+ , GHCupSetError+ , NoDownload+ , NotFoundInPATH+ , PatchFailed+ , UnknownArchive+ , TarDirDoesNotExist+ , TagNotFound+ , DayNotFound+ , NextVerNotFound+ , NoToolVersionSet+ , NotInstalled+ , DirNotEmpty+ , ArchiveResult+ , UninstallFailed+ , MergeFileTreeError+ ]+ compileResult <- run (do+ AppState { ghcupInfo = GHCupInfo { _ghcupDownloads = dls }} <- ask+ hlsVer <- case compopts ^. CompileHLS.gitRef of+ Just ref -> pure (HLS.GitDist (GitBranch ref Nothing))+ Nothing -> do+ -- Compile the version user is pointing to in the tui+ let vi = getVersionInfo (mkTVer lVer) HLS dls+ forM_ (_viPreCompile =<< vi) $ \msg -> do+ logInfo msg+ logInfo+ "...waiting for 5 seconds, you can still abort..."+ liftIO $ threadDelay 5000000 -- for compilation, give the user a sec to intervene+ pure (HLS.SourceDist lVer)++ ghcs <-+ liftE $ forM (compopts ^. CompileHLS.targetGHCs)+ (\ghc -> fmap (_tvVersion . fst) . Utils.fromVersion (Just ghc) $ GHC)+ targetVer <- liftE $ GHCup.compileHLS+ hlsVer+ ghcs+ (compopts ^. CompileHLS.jobs)+ (compopts ^. CompileHLS.overwriteVer)+ (maybe GHCupInternal IsolateDir $ compopts ^. CompileHLS.isolateDir)+ (compopts ^. CompileHLS.cabalProject)+ (compopts ^. CompileHLS.cabalProjectLocal)+ (compopts ^. CompileHLS.updateCabal)+ (compopts ^. CompileHLS.patches)+ (compopts ^. CompileHLS.cabalArgs)+ AppState { ghcupInfo = GHCupInfo { _ghcupDownloads = dls2 }} <- ask+ let vi2 = getVersionInfo (mkTVer targetVer) GHC dls2+ when+ (compopts ^. CompileHLS.setCompile)+ (liftE . void $ GHCup.setHLS targetVer SetHLSOnly Nothing)+ pure (vi2, targetVer)+ )+ case compileResult of+ VRight (vi, tv) -> do+ logInfo "HLS successfully compiled and installed"+ forM_ (_viPostInstall =<< vi) $ \msg -> logInfo msg+ liftIO $ putStr (T.unpack $ prettyVer tv)+ pure $ Right ()+ VLeft err@(V (BuildFailed tmpdir _)) -> pure $ Left $+ case keepDirs (appstate & settings) of+ Never -> prettyHFError err+ _ -> prettyHFError err <> "\n"+ <> "Check the logs at " <> (fromGHCupPath $ appstate & dirs & logsDir)+ <> " and the build directory "+ <> tmpdir <> " for more clues." <> "\n"+ <> "Make sure to clean up " <> tmpdir <> " afterwards."+ VLeft e -> do+ pure $ Left $ prettyHFError e+-- This is the case when the tool is not HLS... which should be impossible but,+-- it exhaustes pattern matches+compileHLS _ (_, ListResult{lTool = _}) = pure (Right ())+++settings' :: IORef AppState+{-# NOINLINE settings' #-}+settings' = unsafePerformIO $ do+ dirs <- getAllDirs+ let loggerConfig = LoggerConfig { lcPrintDebug = False+ , consoleOutter = \_ -> pure ()+ , fileOutter = \_ -> pure ()+ , fancyColors = True+ }+ newIORef $ AppState defaultSettings+ dirs+ defaultKeyBindings+ (GHCupInfo mempty mempty Nothing)+ (PlatformRequest A_64 Darwin Nothing)+ loggerConfig+++getGHCupInfo :: IO (Either String GHCupInfo)+getGHCupInfo = do+ settings <- readIORef settings'++ r <-+ flip runReaderT settings+ . runE @'[DigestError, ContentLengthError, GPGError, JSONError , DownloadFailed , FileDoesNotExistError, StackPlatformDetectError]+ $ do+ pfreq <- lift getPlatformReq+ liftE $ getDownloadsF pfreq++ case r of+ VRight a -> pure $ Right a+ VLeft e -> pure $ Left (prettyHFError e)+++getAppData :: Maybe GHCupInfo+ -> IO (Either String BrickData)+getAppData mgi = runExceptT $ do+ r <- ExceptT $ maybe getGHCupInfo (pure . Right) mgi+ liftIO $ modifyIORef settings' (\s -> s { ghcupInfo = r })+ settings <- liftIO $ readIORef settings'++ flip runReaderT settings $ do+ lV <- listVersions Nothing [] False True (Nothing, Nothing)+ pure $ BrickData (reverse lV)++--++keyHandlers :: KeyBindings+ -> [ ( KeyCombination+ , BrickSettings -> String+ , Brick.EventM Name BrickState ()+ )+ ]+keyHandlers KeyBindings {..} =+ [ (bQuit, const "Quit" , Brick.halt)+ , (bInstall, const "Install" , withIOAction install')+ , (bUninstall, const "Uninstall", withIOAction del')+ , (bSet, const "Set" , withIOAction set')+ , (bChangelog, const "ChangeLog", withIOAction changelog')+ , ( bShowAllVersions+ , \BrickSettings {..} ->+ if _showAllVersions then "Don't show all versions" else "Show all versions"+ , hideShowHandler' (not . _showAllVersions)+ )+ , (bUp, const "Up", Common.zoom appState moveUp)+ , (bDown, const "Down", Common.zoom appState moveDown)+ , (KeyCombination (Vty.KChar 'h') [], const "help", mode .= KeyInfo)+ , (KeyCombination Vty.KEnter [], const "advance options", createMenuforTool )+ ]+ where+ createMenuforTool = do+ e <- use (appState % to sectionListSelectedElement)+ let exitKey = KeyCombination (Vty.KChar 'c') [Vty.MCtrl]+ case e of+ Nothing -> pure ()+ Just (_, r) -> do+ -- Create new ContextMenu, but maintain the state of Install/Compile+ -- menus. This is especially useful in case the user made a typo and+ -- would like to retry the action.+ contextMenu .= ContextMenu.create r exitKey+ -- Set mode to context+ mode .= ContextPanel+ pure ()++ --hideShowHandler' :: (BrickSettings -> Bool) -> (BrickSettings -> Bool) -> m ()+ hideShowHandler' f = do+ app_settings <- use appSettings+ let+ vers = f app_settings+ newAppSettings = app_settings & Common.showAllVersions .~ vers+ ad <- use appData+ current_app_state <- use appState+ appSettings .= newAppSettings+ appState .= constructList ad newAppSettings (Just current_app_state)
+ lib-tui/GHCup/Brick/App.hs view
@@ -0,0 +1,193 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE RankNTypes #-}+{-# OPTIONS_GHC -Wno-unused-record-wildcards #-}+{-# OPTIONS_GHC -Wno-unused-matches #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}++{-+This module defines the brick App. The pattern is very simple:++- Pattern match on the Mode+- Dispatch drawing/events to the corresponding widget/s++In general each widget should know how to draw itself and how to handle its own events, so this+module should only contain:++- how to draw non-widget information. For example the footer+- how to change between modes (widgets aren't aware of the whole application state)++-}++module GHCup.Brick.App where++import qualified GHCup.Brick.Actions as Actions+import qualified GHCup.Brick.Attributes as Attributes+import GHCup.Brick.BrickState (BrickState (..), advanceInstallMenu, appKeys, appSettings, appState, contextMenu, mode, compileGHCMenu, compileHLSMenu)+import GHCup.Brick.Common (Mode (..), Name (..))+import qualified GHCup.Brick.Common as Common+import qualified GHCup.Brick.Widgets.KeyInfo as KeyInfo+import qualified GHCup.Brick.Widgets.Menus.Context as ContextMenu+import qualified GHCup.Brick.Widgets.Navigation as Navigation+import qualified GHCup.Brick.Widgets.Tutorial as Tutorial+import qualified GHCup.Brick.Widgets.Menu as Menu+import qualified GHCup.Brick.Widgets.Menus.AdvanceInstall as AdvanceInstall++import GHCup.Types (AppState (AppState, keyBindings), KeyCombination (KeyCombination))++import qualified Brick.Focus as F+import Brick (+ App (..),+ AttrMap,+ BrickEvent (VtyEvent),+ EventM,+ Widget (..),+ (<=>),+ )+import qualified Brick+import Control.Monad.Reader (+ MonadIO (liftIO),+ void,+ )+import Data.IORef (readIORef)+import Data.List (find, intercalate)+import Prelude hiding (appendFile)++import qualified Graphics.Vty as Vty++import qualified Data.Text as T++import Optics.Getter (to)+import Optics.Operators ((^.))+import Optics.Optic ((%))+import Optics.State (use)+import Optics.State.Operators ((.=))+import qualified GHCup.Brick.Widgets.Menus.CompileGHC as CompileGHC+import qualified GHCup.Brick.Widgets.Menus.CompileHLS as CompileHLS+import Control.Monad (when)++app :: AttrMap -> AttrMap -> App BrickState () Name+app attrs dimAttrs =+ App { appDraw = drawUI dimAttrs+ , appHandleEvent = eventHandler+ , appStartEvent = return ()+ , appAttrMap = const attrs+ , appChooseCursor = Brick.showFirstCursor+ }++drawUI :: AttrMap -> BrickState -> [Widget Name]+drawUI dimAttrs st =+ let+ footer = Brick.withAttr Attributes.helpAttr+ . Brick.txtWrap+ . T.pack+ . foldr1 (\x y -> x <> " " <> y)+ . fmap (\(KeyCombination key mods, pretty_setting, _)+ -> intercalate "+" (Common.showKey key : (Common.showMod <$> mods)) <> ":" <> pretty_setting (st ^. appSettings)+ )+ $ Actions.keyHandlers (st ^. appKeys)+ navg = Navigation.draw dimAttrs (st ^. appState) <=> footer+ in case st ^. mode of+ Navigation -> [navg]+ Tutorial -> [Tutorial.draw, navg]+ KeyInfo -> [KeyInfo.draw (st ^. appKeys), navg]+ ContextPanel -> [ContextMenu.draw (st ^. contextMenu), navg]+ AdvanceInstallPanel -> [AdvanceInstall.draw (st ^. advanceInstallMenu), navg]+ CompileGHCPanel -> [CompileGHC.draw (st ^. compileGHCMenu), navg]+ CompileHLSPanel -> [CompileHLS.draw (st ^. compileHLSMenu), navg]++-- | On q, go back to navigation.+-- On Enter, to go to tutorial+keyInfoHandler :: BrickEvent Name e -> EventM Name BrickState ()+keyInfoHandler ev = case ev of+ VtyEvent (Vty.EvKey (Vty.KChar 'c') [Vty.MCtrl]) -> mode .= Navigation+ VtyEvent (Vty.EvKey Vty.KEnter _ ) -> mode .= Tutorial+ _ -> pure ()++-- | On q, go back to navigation. Else, do nothing+tutorialHandler :: BrickEvent Name e -> EventM Name BrickState ()+tutorialHandler ev =+ case ev of+ VtyEvent (Vty.EvKey (Vty.KChar 'c') [Vty.MCtrl]) -> mode .= Navigation+ _ -> pure ()++-- | Tab/Arrows to navigate.+navigationHandler :: BrickEvent Name e -> EventM Name BrickState ()+navigationHandler ev = do+ AppState { keyBindings = kb } <- liftIO $ readIORef Actions.settings'+ case ev of+ inner_event@(VtyEvent (Vty.EvKey key mods)) ->+ case find (\(key', _, _) -> key' == KeyCombination key mods) (Actions.keyHandlers kb) of+ Just (_, _, handler) -> handler+ Nothing -> void $ Common.zoom appState $ Navigation.handler inner_event+ inner_event -> Common.zoom appState $ Navigation.handler inner_event++contextMenuHandler :: BrickEvent Name e -> EventM Name BrickState ()+contextMenuHandler ev = do+ ctx <- use contextMenu+ let focusedElement = ctx ^. Menu.menuFocusRingL % to F.focusGetCurrent+ (KeyCombination exitKey mods) = ctx ^. Menu.menuExitKeyL+ case (ev, focusedElement) of+ (_ , Nothing) -> pure ()+ (VtyEvent (Vty.EvKey k m), Just n) | k == exitKey && m == mods -> mode .= Navigation+ (VtyEvent (Vty.EvKey Vty.KEnter []), Just (Common.MenuElement Common.AdvanceInstallButton) ) -> mode .= Common.AdvanceInstallPanel+ (VtyEvent (Vty.EvKey Vty.KEnter []), Just (Common.MenuElement Common.CompileGHCButton) ) -> mode .= Common.CompileGHCPanel+ (VtyEvent (Vty.EvKey Vty.KEnter []), Just (Common.MenuElement Common.CompileHLSButton) ) -> mode .= Common.CompileHLSPanel+ _ -> Common.zoom contextMenu $ ContextMenu.handler ev+--+advanceInstallHandler :: BrickEvent Name e -> EventM Name BrickState ()+advanceInstallHandler ev = do+ ctx <- use advanceInstallMenu+ let focusedElement = ctx ^. Menu.menuFocusRingL % to F.focusGetCurrent+ (KeyCombination exitKey mods) = ctx ^. Menu.menuExitKeyL+ case (ev, focusedElement) of+ (_ , Nothing) -> pure ()+ (VtyEvent (Vty.EvKey k m), Just n) | k == exitKey && m == mods -> mode .= ContextPanel+ (VtyEvent (Vty.EvKey Vty.KEnter []), Just (MenuElement Common.OkButton)) -> do+ let iopts = ctx ^. Menu.menuStateL+ when (Menu.isValidMenu ctx) $+ Actions.withIOAction $ Actions.installWithOptions iopts+ _ -> Common.zoom advanceInstallMenu $ AdvanceInstall.handler ev++compileGHCHandler :: BrickEvent Name e -> EventM Name BrickState ()+compileGHCHandler ev = do+ ctx <- use compileGHCMenu+ let focusedElement = ctx ^. Menu.menuFocusRingL % to F.focusGetCurrent+ (KeyCombination exitKey mods) = ctx ^. Menu.menuExitKeyL+ case (ev, focusedElement) of+ (_ , Nothing) -> pure ()+ (VtyEvent (Vty.EvKey k m), Just n) | k == exitKey && m == mods -> mode .= ContextPanel+ (VtyEvent (Vty.EvKey Vty.KEnter []), Just (MenuElement Common.OkButton)) -> do+ let iopts = ctx ^. Menu.menuStateL+ when (Menu.isValidMenu ctx)+ (Actions.withIOAction $ Actions.compileGHC iopts)+ _ -> Common.zoom compileGHCMenu $ CompileGHC.handler ev+++compileHLSHandler :: BrickEvent Name e -> EventM Name BrickState ()+compileHLSHandler ev = do+ ctx <- use compileHLSMenu+ let focusedElement = ctx ^. Menu.menuFocusRingL % to F.focusGetCurrent+ (KeyCombination exitKey mods) = ctx ^. Menu.menuExitKeyL+ case (ev, focusedElement) of+ (_ , Nothing) -> pure ()+ (VtyEvent (Vty.EvKey k m), Just n) | k == exitKey && m == mods -> mode .= ContextPanel+ (VtyEvent (Vty.EvKey Vty.KEnter []), Just (MenuElement Common.OkButton)) -> do+ let iopts = ctx ^. Menu.menuStateL+ when (Menu.isValidMenu ctx)+ (Actions.withIOAction $ Actions.compileHLS iopts)+ _ -> Common.zoom compileHLSMenu $ CompileHLS.handler ev++eventHandler :: BrickEvent Name e -> EventM Name BrickState ()+eventHandler ev = do+ m <- use mode+ case m of+ KeyInfo -> keyInfoHandler ev+ Tutorial -> tutorialHandler ev+ Navigation -> navigationHandler ev+ ContextPanel -> contextMenuHandler ev+ AdvanceInstallPanel -> advanceInstallHandler ev+ CompileGHCPanel -> compileGHCHandler ev+ CompileHLSPanel -> compileHLSHandler ev
+ lib-tui/GHCup/Brick/Attributes.hs view
@@ -0,0 +1,84 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RankNTypes #-}+{-# OPTIONS_GHC -Wno-unused-record-wildcards #-}+{-# OPTIONS_GHC -Wno-unused-matches #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}++{-+This module defined the attributes. Despite of brick's capability to have a hierarchy of attributes, here+we go for the most-simple-approach: a plain hierarchy+-}++module GHCup.Brick.Attributes where++import Brick ( AttrMap)+import qualified Brick+import qualified Brick.Widgets.List as L+import qualified Graphics.Vty as Vty++defaultAttributes :: Bool -> AttrMap+defaultAttributes no_color = Brick.attrMap+ Vty.defAttr+ [ (L.listSelectedFocusedAttr , Vty.defAttr `withBackColor` Vty.blue)+ , (L.listSelectedAttr , Vty.defAttr)+ , (notInstalledAttr , Vty.defAttr `withForeColor` Vty.red)+ , (setAttr , Vty.defAttr `withForeColor` Vty.green)+ , (installedAttr , Vty.defAttr `withForeColor` Vty.green)+ , (recommendedAttr , Vty.defAttr `withForeColor` Vty.green)+ , (hlsPoweredAttr , Vty.defAttr `withForeColor` Vty.green)+ , (latestAttr , Vty.defAttr `withForeColor` Vty.yellow)+ , (latestPrereleaseAttr , Vty.defAttr `withForeColor` Vty.red)+ , (latestNightlyAttr , Vty.defAttr `withForeColor` Vty.red)+ , (prereleaseAttr , Vty.defAttr `withForeColor` Vty.red)+ , (nightlyAttr , Vty.defAttr `withForeColor` Vty.red)+ , (compiledAttr , Vty.defAttr `withForeColor` Vty.brightCyan)+ , (strayAttr , Vty.defAttr `withForeColor` Vty.brightCyan)+ , (dayAttr , Vty.defAttr `withForeColor` Vty.brightCyan)+ , (helpAttr , Vty.defAttr `withStyle` Vty.italic)+ , (hoorayAttr , Vty.defAttr `withForeColor` Vty.brightWhite)+ , (helpMsgAttr , Vty.defAttr `withForeColor` Vty.brightBlack)+ , (errMsgAttr , Vty.defAttr `withForeColor` Vty.red)+ ]+ where+ withForeColor | no_color = const+ | otherwise = Vty.withForeColor+ withBackColor | no_color = \attr _ -> attr `Vty.withStyle` Vty.reverseVideo+ | otherwise = Vty.withBackColor+ withStyle = Vty.withStyle+++notInstalledAttr, setAttr, installedAttr, recommendedAttr, hlsPoweredAttr :: Brick.AttrName+latestAttr, latestPrereleaseAttr, latestNightlyAttr, prereleaseAttr, nightlyAttr :: Brick.AttrName+compiledAttr, strayAttr, dayAttr, helpAttr, hoorayAttr, helpMsgAttr, errMsgAttr :: Brick.AttrName++notInstalledAttr = Brick.attrName "not-installed"+setAttr = Brick.attrName "set"+installedAttr = Brick.attrName "installed"+recommendedAttr = Brick.attrName "recommended"+hlsPoweredAttr = Brick.attrName "hls-powered"+latestAttr = Brick.attrName "latest"+latestPrereleaseAttr = Brick.attrName "latest-prerelease"+latestNightlyAttr = Brick.attrName "latest-nightly"+prereleaseAttr = Brick.attrName "prerelease"+nightlyAttr = Brick.attrName "nightly"+compiledAttr = Brick.attrName "compiled"+strayAttr = Brick.attrName "stray"+dayAttr = Brick.attrName "day"+helpAttr = Brick.attrName "help"+hoorayAttr = Brick.attrName "hooray"+helpMsgAttr = Brick.attrName "helpMsg"+errMsgAttr = Brick.attrName "errMsg"++dimAttributes :: Bool -> AttrMap+dimAttributes no_color = Brick.attrMap+ (Vty.defAttr `Vty.withStyle` Vty.dim)+ [ (Brick.attrName "active" , Vty.defAttr `withBackColor` Vty.blue) -- has no effect ??+ , (Brick.attrName "no-bindist", Vty.defAttr `Vty.withStyle` Vty.dim)+ ]+ where+ withBackColor | no_color = \attr _ -> attr `Vty.withStyle` Vty.reverseVideo+ | otherwise = Vty.withBackColor
+ lib-tui/GHCup/Brick/BrickState.hs view
@@ -0,0 +1,54 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE ViewPatterns #-}+{-# OPTIONS_GHC -Wno-unused-record-wildcards #-}+{-# OPTIONS_GHC -Wno-unused-matches #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE InstanceSigs #-}++{-+This module contains the BrickState. One could be tempted to include this data structure in GHCup.Brick.Common,+but it is better to make a separated module in order to avoid cyclic dependencies.++This happens because the BrickState is sort of a container for all widgets,+but widgets depends on common functionality, hence:++ BrickState `depends on` Widgets.XYZ `depends on` Common++The linear relation above breaks if BrickState is defined in Common.++-}++module GHCup.Brick.BrickState where++import GHCup.Types ( KeyBindings )+import GHCup.Brick.Common ( BrickData(..), BrickSettings(..), Mode(..))+import GHCup.Brick.Widgets.Navigation ( BrickInternalState)+import GHCup.Brick.Widgets.Menus.Context (ContextMenu)+import GHCup.Brick.Widgets.Menus.AdvanceInstall (AdvanceInstallMenu)+import GHCup.Brick.Widgets.Menus.CompileGHC (CompileGHCMenu)+import Optics.TH (makeLenses)+import GHCup.Brick.Widgets.Menus.CompileHLS (CompileHLSMenu)+++data BrickState = BrickState+ { _appData :: BrickData+ , _appSettings :: BrickSettings+ , _appState :: BrickInternalState+ , _contextMenu :: ContextMenu+ , _advanceInstallMenu :: AdvanceInstallMenu+ , _compileGHCMenu :: CompileGHCMenu+ , _compileHLSMenu :: CompileHLSMenu+ , _appKeys :: KeyBindings+ , _mode :: Mode+ }+ --deriving Show++makeLenses ''BrickState
+ lib-tui/GHCup/Brick/Common.hs view
@@ -0,0 +1,225 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE ViewPatterns #-}+{-# OPTIONS_GHC -Wno-unused-record-wildcards #-}+{-# OPTIONS_GHC -Wno-unused-matches #-}+{-# OPTIONS_GHC -Wno-missing-signatures #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE InstanceSigs #-}+{-# LANGUAGE PatternSynonyms #-}++{-+This module contains common values used across the library. Crucially it contains two important types for the brick app:++- Name: List all resources (widgets) used by the app. see https://github.com/jtdaugherty/brick/blob/master/docs/guide.rst#resource-names+- Mode: Use to dispatch events and drawings. see: https://github.com/jtdaugherty/brick/issues/476#issuecomment-1629151920++-}++module GHCup.Brick.Common (+ installedSign,+ setSign,+ notInstalledSign,+ showKey,+ showMod,+ keyToWidget,+ separator,+ frontwardLayer,+ enableScreenReader,+ zoom,+ defaultAppSettings,+ lr,+ showAllVersions,+ Name(..),+ Mode(..),+ BrickData(..),+ BrickSettings(..),+ ResourceId (+ UrlEditBox, SetCheckBox, IsolateEditBox, ForceCheckBox, AdditionalEditBox+ , TargetGhcEditBox, BootstrapGhcEditBox, HadrianGhcEditBox, JobsEditBox, BuildConfigEditBox+ , PatchesEditBox, CrossTargetEditBox, AddConfArgsEditBox, OvewrwiteVerEditBox+ , BuildFlavourEditBox, BuildSystemEditBox, OkButton, AdvanceInstallButton+ , CompileGHCButton, CompileHLSButton, CabalProjectEditBox+ , CabalProjectLocalEditBox, UpdateCabalCheckBox, GitRefEditBox+ ) ) where++import GHCup.List ( ListResult )+import GHCup.Types ( Tool, KeyCombination (KeyCombination) )+import Data.List (intercalate)+import Prelude hiding ( appendFile )+import qualified Graphics.Vty as Vty+import Optics.TH (makeLenses)+import Optics.Lens (toLensVL)+import qualified Brick+import qualified Brick.Widgets.Border as Border+import Brick ((<+>))+import qualified Data.Text as T+import qualified Brick.Widgets.Center as Brick+import qualified Brick.Widgets.Border.Style as Border++-- We could use regular ADTs but different menus share the same options.+-- example: all of ghcup compile ghc, ghcup compile hls, ghcup install cabal, etc...+-- all have a --set, --force, etc... common arguments. If we went for the ADT we'd end up+-- with SetCompileHLSOption, SetCompileGHCOption, SetInstallCabalOption, etc...+-- which isn't terrible, but verbose enough to reject it.++-- | A newtype for labeling resources in menus. It is bundled along with pattern synonyms+newtype ResourceId = ResourceId Int deriving (Eq, Ord, Show)++pattern OkButton :: ResourceId+pattern OkButton = ResourceId 0+pattern AdvanceInstallButton :: ResourceId+pattern AdvanceInstallButton = ResourceId 100+pattern CompileGHCButton :: ResourceId+pattern CompileGHCButton = ResourceId 101+pattern CompileHLSButton :: ResourceId+pattern CompileHLSButton = ResourceId 102++pattern UrlEditBox :: ResourceId+pattern UrlEditBox = ResourceId 1+pattern SetCheckBox :: ResourceId+pattern SetCheckBox = ResourceId 2+pattern IsolateEditBox :: ResourceId+pattern IsolateEditBox = ResourceId 3+pattern ForceCheckBox :: ResourceId+pattern ForceCheckBox = ResourceId 4+pattern AdditionalEditBox :: ResourceId+pattern AdditionalEditBox = ResourceId 5++pattern TargetGhcEditBox :: ResourceId+pattern TargetGhcEditBox = ResourceId 6+pattern BootstrapGhcEditBox :: ResourceId+pattern BootstrapGhcEditBox = ResourceId 7+pattern HadrianGhcEditBox :: ResourceId+pattern HadrianGhcEditBox = ResourceId 17+pattern JobsEditBox :: ResourceId+pattern JobsEditBox = ResourceId 8+pattern BuildConfigEditBox :: ResourceId+pattern BuildConfigEditBox = ResourceId 9+pattern PatchesEditBox :: ResourceId+pattern PatchesEditBox = ResourceId 10+pattern CrossTargetEditBox :: ResourceId+pattern CrossTargetEditBox = ResourceId 11+pattern AddConfArgsEditBox :: ResourceId+pattern AddConfArgsEditBox = ResourceId 12+pattern OvewrwiteVerEditBox :: ResourceId+pattern OvewrwiteVerEditBox = ResourceId 13+pattern BuildFlavourEditBox :: ResourceId+pattern BuildFlavourEditBox = ResourceId 14+pattern BuildSystemEditBox :: ResourceId+pattern BuildSystemEditBox = ResourceId 15++pattern CabalProjectEditBox :: ResourceId+pattern CabalProjectEditBox = ResourceId 16+pattern CabalProjectLocalEditBox :: ResourceId+pattern CabalProjectLocalEditBox = ResourceId 17+pattern UpdateCabalCheckBox :: ResourceId+pattern UpdateCabalCheckBox = ResourceId 18++pattern GitRefEditBox :: ResourceId+pattern GitRefEditBox = ResourceId 19++-- | Name data type. Uniquely identifies each widget in the TUI.+-- some constructors might end up unused, but still is a good practise+-- to have all of them defined, just in case+data Name = AllTools -- ^ The main list widget+ | Singular Tool -- ^ The particular list for each tool+ | KeyInfoBox -- ^ The text box widget with action informacion+ | TutorialBox -- ^ The tutorial widget+ | ContextBox -- ^ The resource for Context Menu+ | CompileGHCBox -- ^ The resource for CompileGHC Menu+ | AdvanceInstallBox -- ^ The resource for AdvanceInstall Menu+ | MenuElement ResourceId -- ^ Each element in a Menu. Resources must not be share for visible+ -- Menus, but MenuA and MenuB can share resources if they both are+ -- invisible, or just one of them is visible.++ deriving (Eq, Ord, Show)++-- | Mode type. It helps to dispatch events to different handlers.+data Mode = Navigation+ | KeyInfo+ | Tutorial+ | ContextPanel+ | AdvanceInstallPanel+ | CompileGHCPanel+ | CompileHLSPanel+ deriving (Eq, Show, Ord)++installedSign :: String+#if IS_WINDOWS+installedSign = "I "+#else+installedSign = "✓ "+#endif++setSign :: String+#if IS_WINDOWS+setSign = "IS"+#else+setSign = "✔✔"+#endif++notInstalledSign :: String+#if IS_WINDOWS+notInstalledSign = "X "+#else+notInstalledSign = "✗ "+#endif+++showKey :: Vty.Key -> String+showKey (Vty.KChar c) = [c]+showKey Vty.KUp = "↑"+showKey Vty.KDown = "↓"+showKey key = tail (show key)++showMod :: Vty.Modifier -> String+showMod = tail . show++-- | Given a KeyComb, produces a string widget with and user friendly text+keyToWidget :: KeyCombination -> Brick.Widget n+keyToWidget (KeyCombination key mods) = Brick.str $ intercalate "+" (showKey key : (showMod <$> mods))++-- | A section separator with max width. Looks like this: -------- o --------+separator :: Brick.Widget n+separator = Border.hBorder <+> Brick.str " o " <+> Border.hBorder++-- | Used to create a layer on top of the main navigation widget (tutorial, info, menus...)+frontwardLayer :: T.Text -> Brick.Widget n -> Brick.Widget n+frontwardLayer layer_name =+ Brick.centerLayer+ . Brick.hLimitPercent 80+ . Brick.vLimitPercent 75+ . Brick.withBorderStyle Border.unicode+ . Border.borderWithLabel (Brick.txt layer_name)++-- | puts a cursor at the line beginning so It can be read by screen readers+enableScreenReader :: n -> Brick.Widget n -> Brick.Widget n+enableScreenReader n = Brick.putCursor n (Brick.Location (0,0))+-- |- tip: when debugging, use Brick.showCursor instead++-- I refuse to give this a type signature.+-- | Given a lens, zoom on it. It is needed because Brick uses microlens but GHCup uses optics.+zoom l = Brick.zoom (toLensVL l)++data BrickData = BrickData+ { _lr :: [ListResult]+ }+ deriving Show++makeLenses ''BrickData++data BrickSettings = BrickSettings { _showAllVersions :: Bool}+ --deriving Show++makeLenses ''BrickSettings++defaultAppSettings :: BrickSettings+defaultAppSettings = BrickSettings False
+ lib-tui/GHCup/Brick/Widgets/KeyInfo.hs view
@@ -0,0 +1,72 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RankNTypes #-}+{-# OPTIONS_GHC -Wno-unused-record-wildcards #-}+{-# OPTIONS_GHC -Wno-unused-matches #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}++{-+A very simple information-only widget with no handler.+-}++module GHCup.Brick.Widgets.KeyInfo where++import GHCup.Types ( KeyBindings(..) )+import qualified GHCup.Brick.Common as Common+++import Brick+ ( Padding(Max),+ Widget(..),+ (<+>),+ (<=>))+import qualified Brick+import Brick.Widgets.Center ( center )+import Prelude hiding ( appendFile )++++draw :: KeyBindings -> Widget Common.Name+draw KeyBindings {..} =+ let+ mkTextBox = Brick.hLimitPercent 70 . Brick.vBox . fmap (Brick.padRight Brick.Max)+ in Common.frontwardLayer "Key Actions"+ $ Brick.vBox [+ center $+ mkTextBox [+ Brick.hBox [+ Brick.txt "Press "+ , Common.keyToWidget bUp, Brick.txt " and ", Common.keyToWidget bDown+ , Brick.txtWrap " to navigate the list of tools"+ ]+ , Brick.hBox [+ Brick.txt "Press "+ , Common.keyToWidget bInstall+ , Brick.txtWrap " to install the selected tool. Notice, you may need to set it as default afterwards"+ ]+ , Brick.hBox [+ Brick.txt "Press "+ , Common.keyToWidget bSet+ , Brick.txtWrap " to set a tool as the one for use"+ ]+ , Brick.hBox [+ Brick.txt "Press "+ , Common.keyToWidget bUninstall+ , Brick.txtWrap " to uninstall a tool"+ ]+ , Brick.hBox [+ Brick.txt "Press "+ , Common.keyToWidget bChangelog+ , Brick.txtWrap " to open the tool's changelog. It will open a web browser"+ ]+ , Brick.hBox [+ Brick.txt "Press "+ , Common.keyToWidget bShowAllVersions+ , Brick.txtWrap " to show older version of each tool"+ ]+ ]+ ]+ <=> Brick.hBox [Brick.txt "Press c+ctrl to return to Navigation" <+> Brick.padRight Brick.Max (Brick.txt " ") <+> Brick.txt "Press Enter to go to the Tutorial"]
+ lib-tui/GHCup/Brick/Widgets/Menu.hs view
@@ -0,0 +1,381 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE ViewPatterns #-}+{-# OPTIONS_GHC -Wno-unused-record-wildcards #-}+{-# OPTIONS_GHC -Wno-unused-matches #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE InstanceSigs #-}+{-# LANGUAGE GADTs #-}+++{- **************++A general system inspired by Brick.Form. It uses optics instead of microlenses and it is less generic than+Brick.Form, but generic enough to serve our purpose.++A Menu consists in+ a) A state value+ b) A list of fields. Each field is capable of modifying a part of the state+ c) some metadata++A field (type MenuField) consists in+ a) a Lens to a part of the Menu state, so the Menu can call that lens to modify its own state+ b) an input widget++An input (type FieldInput) consist in+ a) some state+ b) a validator function+ c) a handler and a renderer++We have to use existential types to achive a composable API since every FieldInput has a different+internal type, and every MenuField has a different Lens. For example:+ - The menu state is a record (MyRecord {uri: URI, flag : Bool})+ - Then, there are two MenuField:+ - One MenuField has (Lens' MyRecord URI) and the other has (Lens' MyRecord Bool)+ - The MenuFields has FieldInputs with internal state Text and Bool, respectively+ - Obviously, the MenuField has to be polimorphic in the Lens' and in the Input internal state,+ But we must hide that polimorphisim (existential), in order to store all MenuField in a List++************** -}++module GHCup.Brick.Widgets.Menu where++import qualified GHCup.Brick.Attributes as Attributes+import qualified GHCup.Brick.Common as Common++import Brick+ ( BrickEvent(..),+ EventM,+ Widget(..),+ (<+>))+import qualified Brick+import qualified Brick.Widgets.Border as Border+import qualified Brick.Widgets.List as L+import qualified Brick.Widgets.Edit as Edit+import Brick.Focus (FocusRing)+import qualified Brick.Focus as F+import Data.Function ( (&))+import Prelude hiding ( appendFile )++import qualified Data.Text as T+++import Optics.TH (makeLensesFor)+import qualified Graphics.Vty as Vty+import Optics.State.Operators ((%=), (.=))+import Optics.Optic ((%))+import Optics.State (use)+import GHCup.Types (KeyCombination)+import Optics (Lens', to, lens)+import Optics.Operators ( (^.), (.~) )+import Data.Foldable (foldl')+++-- | Just some type synonym to make things explicit+type Formatter n = Bool -> Widget n -> Widget n+-- | A label+type Label = T.Text+-- | A help message of an entry+type HelpMessage = T.Text+-- | A button name+type ButtonName n = n++idFormatter :: Formatter n+idFormatter = const id+++-- | An error message+type ErrorMessage = T.Text+data ErrorStatus = Valid | Invalid ErrorMessage deriving (Eq)++-- | A lens which does nothing. Usefull to defined no-op fields+emptyLens :: Lens' s ()+emptyLens = lens (const ()) (\s _ -> s)++-- | A FieldInput is a pair label-content+-- a - is the type of the field it manipulates+-- b - is its internal state (modified in the gui)+-- n - your application's resource name type+data FieldInput a b n =+ FieldInput+ { inputState :: b -- ^ The state of the input field (what's rendered in the screen)+ , inputValidator :: b -> Either ErrorMessage a -- ^ A validator function+ , inputHelp :: HelpMessage -- ^ The input helpMessage+ , inputRender :: Bool+ -> ErrorStatus+ -> HelpMessage+ -> b+ -> (Widget n -> Widget n)+ -> Widget n -- ^ How to draw the input, with focus a help message and input.+ -- A extension function can be applied too+ , inputHandler :: BrickEvent n () -> EventM n b () -- ^ The handler+ }++makeLensesFor+ [ ("inputState", "inputStateL")+ , ("inputValidator", "inputValidatorL")+ , ("inputName", "inputNameL")+ , ("inputHelp", "inputHelpL")+ ]+ ''FieldInput++-- | The MenuField is an existential type which stores a Lens' to a part of the Menu state.+-- In also contains a Field input which internal state is hidden+data MenuField s n where+ MenuField ::+ { fieldAccesor :: Lens' s a -- ^ A Lens pointing to some part of the state+ , fieldInput :: FieldInput a b n -- ^ The input which modifies the state+ , fieldLabel :: Label -- ^ The label+ , fieldStatus :: ErrorStatus -- ^ Whether the current is valid or not.+ , fieldName :: n+ } -> MenuField s n++isValidField :: MenuField s n -> Bool+isValidField = (== Valid) . fieldStatus++makeLensesFor+ [ ("fieldLabel", "fieldLabelL")+ , ("fieldStatus", "fieldStatusL")+ ]+ ''MenuField++-- | A fancy lens to the help message+fieldHelpMsgL :: Lens' (MenuField s n) HelpMessage+fieldHelpMsgL = lens g s+ where g (MenuField {..})= fieldInput ^. inputHelpL+ s (MenuField{..}) msg = MenuField {fieldInput = fieldInput & inputHelpL .~ msg , ..}++-- | How to draw a field given a formater+drawField :: Formatter n -> Bool -> MenuField s n -> Widget n+drawField amp focus (MenuField { fieldInput = FieldInput {..}, ..}) =+ let input = inputRender focus fieldStatus inputHelp inputState (amp focus)+ in if focus+ then Common.enableScreenReader fieldName $ Brick.visible input+ else input++instance Brick.Named (MenuField s n) n where+ getName :: MenuField s n -> n+ getName entry = entry & fieldName+++{- *****************+ CheckBox widget+***************** -}++type CheckBoxField = MenuField++createCheckBoxInput :: FieldInput Bool Bool n+createCheckBoxInput = FieldInput False Right "" checkBoxRender checkBoxHandler+ where+ border w = Brick.txt "[" <+> (Brick.padRight (Brick.Pad 1) $ Brick.padLeft (Brick.Pad 2) w) <+> Brick.txt "]"+ drawBool b =+ if b+ then border . Brick.withAttr Attributes.installedAttr $ Brick.str Common.installedSign+ else border . Brick.withAttr Attributes.notInstalledAttr $ Brick.str Common.notInstalledSign+ checkBoxRender focus _ help check f =+ let core = f $ drawBool check+ in if focus+ then core+ else core <+> (Brick.padLeft (Brick.Pad 1) . renderAsHelpMsg $ help)+ checkBoxHandler = \case+ VtyEvent (Vty.EvKey Vty.KEnter []) -> Brick.modify not+ _ -> pure ()++createCheckBoxField :: n -> Lens' s Bool -> CheckBoxField s n+createCheckBoxField name access = MenuField access createCheckBoxInput "" Valid name++{- *****************+ Editable widget+***************** -}++type EditableField = MenuField++createEditableInput :: (Ord n, Show n) => n -> (T.Text -> Either ErrorMessage a) -> FieldInput a (Edit.Editor T.Text n) n+createEditableInput name validator = FieldInput initEdit validateEditContent "" drawEdit Edit.handleEditorEvent+ where+ drawEdit focus errMsg help edi amp =+ let+ borderBox w = amp (Brick.vLimit 1 $ Border.vBorder <+> Brick.padRight Brick.Max w <+> Border.vBorder)+ editorRender = Edit.renderEditor (Brick.txt . T.unlines) focus edi+ isEditorEmpty = Edit.getEditContents edi == [mempty]+ in case errMsg of+ Valid | isEditorEmpty -> borderBox $ renderAsHelpMsg help+ | otherwise -> borderBox editorRender+ Invalid msg+ | focus && isEditorEmpty -> borderBox $ renderAsHelpMsg help+ | focus -> borderBox editorRender+ | otherwise -> borderBox $ renderAsErrMsg msg+ validateEditContent = validator . T.init . T.unlines . Edit.getEditContents+ initEdit = Edit.editorText name (Just 1) ""++createEditableField :: (Eq n, Ord n, Show n) => n -> (T.Text -> Either ErrorMessage a) -> Lens' s a -> EditableField s n+createEditableField name validator access = MenuField access input "" Valid name+ where+ input = createEditableInput name validator++{- *****************+ Button widget+***************** -}++type Button = MenuField++createButtonInput :: FieldInput () () n+createButtonInput = FieldInput () Right "" drawButton (const $ pure ())+ where+ drawButton True (Invalid err) _ _ amp = amp . centerV . renderAsErrMsg $ err+ drawButton _ _ help _ amp = amp . centerV . renderAsHelpMsg $ help++createButtonField :: n -> Button s n+createButtonField = MenuField emptyLens createButtonInput "" Valid++{- *****************+ Utilities+***************** -}++-- | highlights a widget (using List.listSelectedFocusedAttr)+highlighted :: Widget n -> Widget n+highlighted = Brick.withAttr L.listSelectedFocusedAttr++-- | Given a text, crates a highlighted label on focus. An amplifier can be passed+renderAslabel :: T.Text -> Bool -> Widget n+renderAslabel t focus =+ if focus+ then highlighted $ Brick.txt t+ else Brick.txt t++-- | Creates a left align column.+-- Example: |- col2 is align dispite the length of col1+-- row1_col1 row1_col2+-- row2_col1_large row2_col2+leftify :: Int -> Brick.Widget n -> Brick.Widget n+leftify i = Brick.hLimit i . Brick.padRight Brick.Max++-- | Creates a right align column.+-- Example: |- col2 is align dispite the length of col1+-- row1_col1 row1_col2+-- row2_col1_large row2_col2+rightify :: Int -> Brick.Widget n -> Brick.Widget n+rightify i = Brick.hLimit i . Brick.padLeft Brick.Max++-- | center a line in three rows.+centerV :: Widget n -> Widget n+centerV = Brick.padTopBottom 1++-- | render some Text using helpMsgAttr+renderAsHelpMsg :: T.Text -> Widget n+renderAsHelpMsg = Brick.withAttr Attributes.helpMsgAttr . Brick.txt++-- | render some Text using errMsgAttr+renderAsErrMsg :: T.Text -> Widget n+renderAsErrMsg = Brick.withAttr Attributes.errMsgAttr . Brick.txt++{- *****************+ Menu widget+***************** -}++-- | A menu is a list of Fields and a state. Informally we can think about s in terms of the record type returned by+-- a form.+data Menu s n+ = Menu+ { menuFields :: [MenuField s n] -- ^ The datatype representing the list of entries. Precisely, any array-like data type is highly unconvinient.+ , menuState :: s+ , menuValidator :: s -> Maybe ErrorMessage -- ^ A validator function+ , menuButtons :: [Button s n] -- ^ The buttons. Commonly, the handlers for buttons are defined outside the menu handler.+ , menuFocusRing :: FocusRing n -- ^ The focus ring with the resource name for each entry and each button, in the order you want to loop them.+ , menuExitKey :: KeyCombination -- ^ The key to exit the Menu+ , menuName :: n -- ^ The resource Name.+ }++makeLensesFor+ [ ("menuFields", "menuFieldsL"), ("menuState", "menuStateL"), ("menuValidator", "menuValidatorL")+ , ("menuButtons", "menuButtonsL"), ("menuFocusRing", "menuFocusRingL")+ , ("menuExitKey", "menuExitKeyL"), ("menuName", "menuNameL")+ ]+ ''Menu++isValidMenu :: Menu s n -> Bool+isValidMenu m = (all isValidField $ menuFields m)+ && (case (menuValidator m) (menuState m) of { Nothing -> True; _ -> False })++createMenu :: n -> s -> (s -> Maybe ErrorMessage)+ -> KeyCombination -> [Button s n] -> [MenuField s n] -> Menu s n+createMenu n initial validator exitK buttons fields = Menu fields initial validator buttons ring exitK n+ where ring = F.focusRing $ [field & fieldName | field <- fields] ++ [button & fieldName | button <- buttons]++handlerMenu :: forall n e s. Eq n => BrickEvent n e -> EventM n (Menu s n) ()+handlerMenu ev =+ case ev of+ VtyEvent (Vty.EvKey (Vty.KChar '\t') []) -> menuFocusRingL %= F.focusNext+ VtyEvent (Vty.EvKey Vty.KBackTab []) -> menuFocusRingL %= F.focusPrev+ VtyEvent (Vty.EvKey Vty.KDown []) -> menuFocusRingL %= F.focusNext+ VtyEvent (Vty.EvKey Vty.KUp []) -> menuFocusRingL %= F.focusPrev+ VtyEvent e -> do+ focused <- use $ menuFocusRingL % to F.focusGetCurrent+ fields <- use menuFieldsL+ case focused of+ Nothing -> pure ()+ Just n -> do+ updated_fields <- updateFields n (VtyEvent e) fields+ validator <- use menuValidatorL+ state <- use menuStateL+ if all isValidField updated_fields+ then case validator state of+ Nothing -> menuButtonsL %= fmap (fieldStatusL .~ Valid)+ Just err -> menuButtonsL %= fmap (fieldStatusL .~ Invalid err)+ else menuButtonsL %= fmap (fieldStatusL .~ Invalid "Some fields are invalid")+ menuFieldsL .= updated_fields+ _ -> pure ()+ where+ -- runs the Event with the inner handler of MenuField.+ updateFields :: n -> BrickEvent n () -> [MenuField s n] -> EventM n (Menu s n) [MenuField s n]+ updateFields n e = traverse $ \x@(MenuField {fieldInput = FieldInput {..}, ..}) ->+ if Brick.getName x == n+ then do+ newb <- Brick.nestEventM' inputState (inputHandler e)+ let newField = MenuField {fieldInput = (FieldInput {inputState=newb, ..}) , ..}+ case inputValidator newb of+ Left errmsg -> pure $ newField & fieldStatusL .~ Invalid errmsg+ Right a -> menuStateL % fieldAccesor .= a >> pure (newField & fieldStatusL .~ Valid)+ else pure x+++drawMenu :: (Eq n, Ord n, Show n, Brick.Named (MenuField s n) n) => Menu s n -> Widget n+drawMenu menu =+ Brick.vBox+ [ Brick.vBox buttonWidgets+ , Common.separator+ , Brick.vLimit (length fieldLabels) $ Brick.withVScrollBars Brick.OnRight+ $ Brick.viewport (menu ^. menuNameL) Brick.Vertical+ $ Brick.vBox fieldWidgets+ , Brick.txt " "+ , Brick.padRight Brick.Max $+ Brick.txt "Press "+ <+> Common.keyToWidget (menu ^. menuExitKeyL)+ <+> Brick.txt " to go back"+ ]+ where+ fieldLabels = [field & fieldLabel | field <- menu ^. menuFieldsL]+ buttonLabels = [button & fieldLabel | button <- menu ^. menuButtonsL]+ allLabels = fieldLabels ++ buttonLabels++ maxWidth = foldl' max 5 (fmap Brick.textWidth allLabels)++ -- A list of functions which draw a highlighted label with right padding at the left of a widget.+ amplifiers =+ let labelsWidgets = fmap renderAslabel fieldLabels+ in fmap (\f b -> ((rightify (maxWidth + 1) (f b <+> Brick.txt " ")) <+>) ) labelsWidgets+ drawFields = fmap drawField amplifiers+ fieldWidgets = zipWith (F.withFocusRing (menu ^. menuFocusRingL)) drawFields (menu ^. menuFieldsL)++ buttonAmplifiers =+ let buttonAsWidgets = fmap renderAslabel buttonLabels+ in fmap (\f b -> ((leftify (maxWidth + 2) . Border.border $ f b) <+>) ) buttonAsWidgets+ drawButtons = fmap drawField buttonAmplifiers+ buttonWidgets = zipWith (F.withFocusRing (menu ^. menuFocusRingL)) drawButtons (menu ^. menuButtonsL)
+ lib-tui/GHCup/Brick/Widgets/Menus/AdvanceInstall.hs view
@@ -0,0 +1,118 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE ViewPatterns #-}+{-# OPTIONS_GHC -Wno-unused-record-wildcards #-}+{-# OPTIONS_GHC -Wno-unused-matches #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE InstanceSigs #-}+{-# OPTIONS_GHC -Wno-incomplete-patterns #-}++module GHCup.Brick.Widgets.Menus.AdvanceInstall (+ InstallOptions (..),+ AdvanceInstallMenu,+ create,+ handler,+ draw,+ instBindistL,+ instSetL,+ isolateDirL,+ forceInstallL,+ addConfArgsL,+) where++import GHCup.Brick.Widgets.Menu (Menu)+import qualified GHCup.Brick.Widgets.Menu as Menu+import GHCup.Brick.Common(Name(..))+import Brick+ ( BrickEvent(..),+ EventM,+ Widget(..))+import Prelude hiding ( appendFile )+import Optics.TH (makeLensesFor)+import qualified GHCup.Brick.Common as Common+import GHCup.Types (KeyCombination)+import URI.ByteString (URI)+import qualified Data.Text as T+import Data.Bifunctor (Bifunctor(..))+import Data.Function ((&))+import Optics ((.~))+import Data.Char (isSpace)+import qualified GHCup.Utils.Parsers as Utils++data InstallOptions = InstallOptions+ { instBindist :: Maybe URI+ , instSet :: Bool+ , isolateDir :: Maybe FilePath+ , forceInstall :: Bool+ , addConfArgs :: [T.Text]+ } deriving (Eq, Show)++makeLensesFor [+ ("instBindist", "instBindistL")+ , ("instSet", "instSetL")+ , ("isolateDir", "isolateDirL")+ , ("forceInstall", "forceInstallL")+ , ("addConfArgs", "addConfArgsL")+ ]+ ''InstallOptions++type AdvanceInstallMenu = Menu InstallOptions Name++create :: KeyCombination -> AdvanceInstallMenu+create k = Menu.createMenu AdvanceInstallBox initialState validator k [ok] fields+ where+ initialState = InstallOptions Nothing False Nothing False []+ validator InstallOptions {..} = case (instSet, isolateDir) of+ (True, Just _) -> Just "Cannot set active when doing an isolated install"+ _ -> Nothing+ -- Brick's internal editor representation is [mempty].+ emptyEditor i = T.null i || (i == "\n")++ whenEmpty :: a -> (T.Text -> Either Menu.ErrorMessage a) -> T.Text -> Either Menu.ErrorMessage a+ whenEmpty emptyval f i = if not (emptyEditor i) then f i else Right emptyval++ uriValidator :: T.Text -> Either Menu.ErrorMessage (Maybe URI)+ uriValidator = whenEmpty Nothing (second Just . readUri)+ where readUri = first T.pack . Utils.uriParser . T.unpack++ filepathValidator :: T.Text -> Either Menu.ErrorMessage (Maybe FilePath)+ filepathValidator = whenEmpty Nothing (bimap T.pack Just . Utils.absolutePathParser . T.unpack)++ additionalValidator :: T.Text -> Either Menu.ErrorMessage [T.Text]+ additionalValidator = Right . T.split isSpace++ fields =+ [ Menu.createEditableField (Common.MenuElement Common.UrlEditBox) uriValidator instBindistL+ & Menu.fieldLabelL .~ "url"+ & Menu.fieldHelpMsgL .~ "Install the specified version from this bindist"+ , Menu.createCheckBoxField (Common.MenuElement Common.SetCheckBox) instSetL+ & Menu.fieldLabelL .~ "set"+ & Menu.fieldHelpMsgL .~ "Set as active version after install"+ , Menu.createEditableField (Common.MenuElement Common.IsolateEditBox) filepathValidator isolateDirL+ & Menu.fieldLabelL .~ "isolated"+ & Menu.fieldHelpMsgL .~ "install in an isolated absolute directory instead of the default one"+ , Menu.createCheckBoxField (Common.MenuElement Common.ForceCheckBox) forceInstallL+ & Menu.fieldLabelL .~ "force"+ & Menu.fieldHelpMsgL .~ "Force install (THIS IS UNSAFE, only use it in Dockerfiles or CI)"+ , Menu.createEditableField (Common.MenuElement Common.AdditionalEditBox) additionalValidator addConfArgsL+ & Menu.fieldLabelL .~ "CONFIGURE_ARGS"+ & Menu.fieldHelpMsgL .~ "Additional arguments to bindist configure"+ ]++ ok = Menu.createButtonField (Common.MenuElement Common.OkButton)+ & Menu.fieldLabelL .~ "Advance Install"+ & Menu.fieldHelpMsgL .~ "Install with options below"++handler :: BrickEvent Name e -> EventM Name AdvanceInstallMenu ()+handler = Menu.handlerMenu+++draw :: AdvanceInstallMenu -> Widget Name+draw = Common.frontwardLayer "Advance Install" . Menu.drawMenu
+ lib-tui/GHCup/Brick/Widgets/Menus/CompileGHC.hs view
@@ -0,0 +1,219 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE ViewPatterns #-}+{-# OPTIONS_GHC -Wno-unused-record-wildcards #-}+{-# OPTIONS_GHC -Wno-unused-matches #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE InstanceSigs #-}+{-# OPTIONS_GHC -Wno-incomplete-patterns #-}++module GHCup.Brick.Widgets.Menus.CompileGHC (+ CompileGHCOptions,+ CompileGHCMenu,+ create,+ handler,+ draw,+ bootstrapGhc,+ hadrianGhc,+ jobs,+ buildConfig,+ patches,+ crossTarget,+ addConfArgs,+ setCompile,+ overwriteVer,+ buildFlavour,+ buildSystem,+ isolateDir,+ gitRef,+) where++import GHCup.Brick.Widgets.Menu (Menu)+import qualified GHCup.Brick.Widgets.Menu as Menu+import GHCup.Brick.Common(Name(..))+import Brick+ ( BrickEvent(..),+ EventM,+ Widget(..))+import Prelude hiding ( appendFile )+import Optics.TH (makeLenses)+import qualified GHCup.Brick.Common as Common+import GHCup.Types+ ( KeyCombination, BuildSystem(..), VersionPattern )+import URI.ByteString (URI)+import qualified Data.Text as T+import Data.Bifunctor (Bifunctor(..))+import Data.Function ((&))+import Optics ((.~))+import Data.Char (isSpace)+import Data.Versions (Version, version)+import System.FilePath (isPathSeparator)+import Control.Applicative (Alternative((<|>)))+import Text.Read (readEither)+import qualified GHCup.Utils.Parsers as Utils++data CompileGHCOptions = CompileGHCOptions+ { _bootstrapGhc :: Either Version FilePath+ , _hadrianGhc :: Maybe (Either Version FilePath)+ , _jobs :: Maybe Int+ , _buildConfig :: Maybe FilePath+ , _patches :: Maybe (Either FilePath [URI])+ , _crossTarget :: Maybe T.Text+ , _addConfArgs :: [T.Text]+ , _setCompile :: Bool+ , _overwriteVer :: Maybe [VersionPattern]+ , _buildFlavour :: Maybe String+ , _buildSystem :: Maybe BuildSystem+ , _isolateDir :: Maybe FilePath+ , _gitRef :: Maybe String+ } deriving (Eq, Show)++makeLenses ''CompileGHCOptions++type CompileGHCMenu = Menu CompileGHCOptions Name++create :: KeyCombination -> CompileGHCMenu+create k = Menu.createMenu CompileGHCBox initialState validator k buttons fields+ where+ initialState =+ CompileGHCOptions+ (Right "")+ Nothing+ Nothing+ Nothing+ Nothing+ Nothing+ []+ False+ Nothing+ Nothing+ Nothing+ Nothing+ Nothing+ validator CompileGHCOptions {..} = case (_setCompile, _isolateDir) of+ (True, Just _) -> Just "Cannot set active when doing an isolated install"+ _ -> case (_buildConfig, _buildSystem) of+ (Just _, Just Hadrian) -> Just "Build config can be specified only for make build system"+ _ -> Nothing+ -- Brick's internal editor representation is [mempty].+ emptyEditor i = T.null i || (i == "\n")+ whenEmpty :: a -> (T.Text -> Either Menu.ErrorMessage a) -> T.Text -> Either Menu.ErrorMessage a+ whenEmpty emptyval f i = if not (emptyEditor i) then f i else Right emptyval++ bootstrapV :: T.Text -> Either Menu.ErrorMessage (Either Version FilePath)+ bootstrapV i =+ case not $ emptyEditor i of+ True ->+ let readVersion = bimap (const "Not a valid version") Left (version i)+ readPath = do+ mfilepath <- filepathV i+ case mfilepath of+ Nothing -> Left "Invalid Empty value"+ Just f -> Right (Right f)+ in if T.any isPathSeparator i+ then readPath+ else readVersion+ False -> Left "Invalid Empty value"++ hadrianstrapV :: T.Text -> Either Menu.ErrorMessage (Maybe (Either Version FilePath))+ hadrianstrapV i' =+ let readVersion = bimap (const "Not a valid version") (Just . Left) . version+ readPath = bimap T.pack (Just . Right) . Utils.absolutePathParser . T.unpack+ in if T.any isPathSeparator i'+ then whenEmpty Nothing readPath i'+ else whenEmpty Nothing readVersion i'++ versionV :: T.Text -> Either Menu.ErrorMessage (Maybe [VersionPattern])+ versionV = whenEmpty Nothing (bimap T.pack Just . Utils.overWriteVersionParser . T.unpack)++ jobsV :: T.Text -> Either Menu.ErrorMessage (Maybe Int)+ jobsV =+ let parseInt = bimap (const "Invalid value. Must be an integer") Just . readEither @Int . T.unpack+ in whenEmpty Nothing parseInt++ patchesV :: T.Text -> Either Menu.ErrorMessage (Maybe (Either FilePath [URI]))+ patchesV = whenEmpty Nothing readPatches+ where+ readPatches j =+ let+ x = second (Just . Left) $ Utils.absolutePathParser (T.unpack j)+ y = second (Just . Right) $ traverse (Utils.uriParser . T.unpack) (T.split isSpace j)+ in first T.pack $ x <|> y++ filepathV :: T.Text -> Either Menu.ErrorMessage (Maybe FilePath)+ filepathV = whenEmpty Nothing (bimap T.pack Just . Utils.absolutePathParser . T.unpack)++ additionalValidator :: T.Text -> Either Menu.ErrorMessage [T.Text]+ additionalValidator = Right . T.split isSpace++ systemV :: T.Text -> Either Menu.ErrorMessage (Maybe BuildSystem)+ systemV = whenEmpty Nothing readSys+ where+ readSys i+ | T.toLower i == "hadrian" = Right $ Just Hadrian+ | T.toLower i == "make" = Right $ Just Make+ | otherwise = Left "Not a valid Build System"++ fields =+ [ Menu.createEditableField (Common.MenuElement Common.BootstrapGhcEditBox) bootstrapV bootstrapGhc+ & Menu.fieldLabelL .~ "bootstrap-ghc"+ & Menu.fieldHelpMsgL .~ "The GHC version (or full path) to bootstrap with (must be installed)"+ & Menu.fieldStatusL .~ Menu.Invalid "Invalid Empty value"+ , Menu.createEditableField (Common.MenuElement Common.HadrianGhcEditBox) hadrianstrapV hadrianGhc+ & Menu.fieldLabelL .~ "hadrian-ghc"+ & Menu.fieldHelpMsgL .~ "The GHC version (or full path) to GHC that will be used to compile hadrian (must be installed)"+ , Menu.createEditableField (Common.MenuElement Common.JobsEditBox) jobsV jobs+ & Menu.fieldLabelL .~ "jobs"+ & Menu.fieldHelpMsgL .~ "How many jobs to use for make"+ , Menu.createCheckBoxField (Common.MenuElement Common.SetCheckBox) setCompile+ & Menu.fieldLabelL .~ "set"+ & Menu.fieldHelpMsgL .~ "Set as active version after install"+ , Menu.createEditableField (Common.MenuElement Common.BuildFlavourEditBox) (Right . Just . T.unpack) buildFlavour+ & Menu.fieldLabelL .~ "flavour"+ & Menu.fieldHelpMsgL .~ "Set the compile build flavour (this value depends on the build system type: 'make' vs 'hadrian')"+ , Menu.createEditableField (Common.MenuElement Common.AdditionalEditBox) additionalValidator addConfArgs+ & Menu.fieldLabelL .~ "CONFIGURE_ARGS"+ & Menu.fieldHelpMsgL .~ "Additional arguments to compile configure"+ , Menu.createEditableField (Common.MenuElement Common.BuildConfigEditBox) filepathV buildConfig+ & Menu.fieldLabelL .~ "build config"+ & Menu.fieldHelpMsgL .~ "Absolute path to build config file (make build system only)"+ , Menu.createEditableField (Common.MenuElement Common.PatchesEditBox) patchesV patches+ & Menu.fieldLabelL .~ "patches"+ & Menu.fieldHelpMsgL .~ "Either a URI to a patch (https/http/file) or Absolute path to patch directory"+ , Menu.createEditableField (Common.MenuElement Common.CrossTargetEditBox) (Right . Just) crossTarget+ & Menu.fieldLabelL .~ "cross target"+ & Menu.fieldHelpMsgL .~ "Build cross-compiler for this platform"+ , Menu.createEditableField (Common.MenuElement Common.BuildSystemEditBox) systemV buildSystem+ & Menu.fieldLabelL .~ "build system"+ & Menu.fieldHelpMsgL .~ "either 'make' or 'hadrian'"+ , Menu.createEditableField (Common.MenuElement Common.OvewrwiteVerEditBox) versionV overwriteVer+ & Menu.fieldLabelL .~ "overwrite-version"+ & Menu.fieldHelpMsgL .~ "Allows to overwrite the finally installed VERSION with a different one"+ , Menu.createEditableField (Common.MenuElement Common.IsolateEditBox) filepathV isolateDir+ & Menu.fieldLabelL .~ "isolated"+ & Menu.fieldHelpMsgL .~ "install in an isolated absolute directory instead of the default one"+ , Menu.createEditableField (Common.MenuElement Common.GitRefEditBox) (Right . Just . T.unpack) gitRef+ & Menu.fieldLabelL .~ "git-ref"+ & Menu.fieldHelpMsgL .~ "The git commit/branch/ref to build from"+ ]++ buttons = [+ Menu.createButtonField (Common.MenuElement Common.OkButton)+ & Menu.fieldLabelL .~ "Compile"+ & Menu.fieldHelpMsgL .~ "Compile GHC from source with options below"+ & Menu.fieldStatusL .~ Menu.Invalid "bootstrap GHC is mandatory"+ ]++handler :: BrickEvent Name e -> EventM Name CompileGHCMenu ()+handler = Menu.handlerMenu+++draw :: CompileGHCMenu -> Widget Name+draw = Common.frontwardLayer "Compile GHC" . Menu.drawMenu
+ lib-tui/GHCup/Brick/Widgets/Menus/CompileHLS.hs view
@@ -0,0 +1,190 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE ViewPatterns #-}+{-# OPTIONS_GHC -Wno-unused-record-wildcards #-}+{-# OPTIONS_GHC -Wno-unused-matches #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE InstanceSigs #-}+{-# OPTIONS_GHC -Wno-incomplete-patterns #-}++module GHCup.Brick.Widgets.Menus.CompileHLS (+ CompileHLSOptions,+ CompileHLSMenu,+ create,+ handler,+ draw,+ jobs,+ setCompile,+ updateCabal,+ overwriteVer,+ isolateDir,+ cabalProject,+ cabalProjectLocal,+ patches,+ targetGHCs,+ cabalArgs,+ gitRef,+)+where++import GHCup.Brick.Widgets.Menu (Menu)+import qualified GHCup.Brick.Widgets.Menu as Menu+import GHCup.Brick.Common(Name(..))+import Brick+ ( BrickEvent(..),+ EventM,+ Widget(..))+import Prelude hiding ( appendFile )+import Optics.TH (makeLenses)+import qualified GHCup.Brick.Common as Common+import GHCup.Types (KeyCombination, VersionPattern, ToolVersion)+import URI.ByteString (URI)+import qualified Data.Text as T+import Data.Bifunctor (Bifunctor(..))+import Data.Function ((&))+import Optics ((.~))+import Data.Char (isSpace)+import Control.Applicative (Alternative((<|>)))+import Text.Read (readEither)+import qualified GHCup.Utils.Parsers as Utils++data CompileHLSOptions = CompileHLSOptions+ { _jobs :: Maybe Int+ , _setCompile :: Bool+ , _updateCabal :: Bool+ , _overwriteVer :: Maybe [VersionPattern]+ , _isolateDir :: Maybe FilePath+ , _cabalProject :: Maybe (Either FilePath URI)+ , _cabalProjectLocal :: Maybe URI+ , _patches :: Maybe (Either FilePath [URI])+ , _targetGHCs :: [ToolVersion]+ , _cabalArgs :: [T.Text]+ , _gitRef :: Maybe String+ } deriving (Eq, Show)++makeLenses ''CompileHLSOptions++type CompileHLSMenu = Menu CompileHLSOptions Name++create :: KeyCombination -> CompileHLSMenu+create k = Menu.createMenu CompileGHCBox initialState validator k buttons fields+ where+ initialState =+ CompileHLSOptions+ Nothing+ False+ False+ Nothing+ Nothing+ Nothing+ Nothing+ Nothing+ []+ []+ Nothing++ validator CompileHLSOptions {..} = case (_setCompile, _isolateDir) of+ (True, Just _) -> Just "Cannot set active when doing an isolated install"+ _ -> if null _targetGHCs+ then Just "Specify at least one valid target GHC"+ else Nothing+ -- Brick's internal editor representation is [mempty].+ emptyEditor i = T.null i || (i == "\n")+ whenEmpty :: a -> (T.Text -> Either Menu.ErrorMessage a) -> T.Text -> Either Menu.ErrorMessage a+ whenEmpty emptyval f i = if not (emptyEditor i) then f i else Right emptyval++ readUri :: T.Text -> Either Menu.ErrorMessage URI+ readUri = first T.pack . Utils.uriParser . T.unpack++ cabalProjectV :: T.Text -> Either Menu.ErrorMessage (Maybe (Either FilePath URI))+ cabalProjectV = whenEmpty Nothing parseFileOrUri+ where+ parseFileOrUri i =+ let x = bimap T.unpack Right (readUri i)+ y = Right . Left . T.unpack $ i+ in bimap T.pack Just $ x <|> y++ cabalProjectLocalV :: T.Text -> Either Menu.ErrorMessage (Maybe URI)+ cabalProjectLocalV = whenEmpty Nothing (second Just . readUri)++ ghcVersionTagEither :: T.Text -> Either Menu.ErrorMessage [ToolVersion]+ ghcVersionTagEither = whenEmpty [] $ first T.pack . traverse (Utils.ghcVersionTagEither . T.unpack) . T.split isSpace++ overWriteVersionParser :: T.Text -> Either Menu.ErrorMessage (Maybe [VersionPattern])+ overWriteVersionParser = whenEmpty Nothing $ bimap T.pack Just . Utils.overWriteVersionParser . T.unpack++ jobsV :: T.Text -> Either Menu.ErrorMessage (Maybe Int)+ jobsV =+ let parseInt = bimap (const "Invalid value. Must be an integer") Just . readEither @Int . T.unpack+ in whenEmpty Nothing parseInt++ patchesV :: T.Text -> Either Menu.ErrorMessage (Maybe (Either FilePath [URI]))+ patchesV = whenEmpty Nothing readPatches+ where+ readPatches j =+ let+ x = second (Just . Left) $ Utils.absolutePathParser (T.unpack j)+ y = second (Just . Right) $ traverse (Utils.uriParser . T.unpack) (T.split isSpace j)+ in first T.pack $ x <|> y++ filepathV :: T.Text -> Either Menu.ErrorMessage (Maybe FilePath)+ filepathV = whenEmpty Nothing (bimap T.pack Just . Utils.isolateParser . T.unpack)++ additionalValidator :: T.Text -> Either Menu.ErrorMessage [T.Text]+ additionalValidator = Right . T.split isSpace++ fields =+ [ Menu.createCheckBoxField (Common.MenuElement Common.UpdateCabalCheckBox) updateCabal+ & Menu.fieldLabelL .~ "cabal update"+ & Menu.fieldHelpMsgL .~ "Run 'cabal update' before the build"+ , Menu.createEditableField (Common.MenuElement Common.JobsEditBox) jobsV jobs+ & Menu.fieldLabelL .~ "jobs"+ & Menu.fieldHelpMsgL .~ "How many jobs to use for make"+ , Menu.createEditableField (Common.MenuElement Common.TargetGhcEditBox) ghcVersionTagEither targetGHCs+ & Menu.fieldLabelL .~ "target GHC(s)"+ & Menu.fieldHelpMsgL .~ "space separated list of GHC versions to compile for"+ , Menu.createCheckBoxField (Common.MenuElement Common.SetCheckBox) setCompile+ & Menu.fieldLabelL .~ "set"+ & Menu.fieldHelpMsgL .~ "Set as active version after install"+ , Menu.createEditableField (Common.MenuElement Common.AdditionalEditBox) additionalValidator cabalArgs+ & Menu.fieldLabelL .~ "CABAL_ARGS"+ & Menu.fieldHelpMsgL .~ "Additional arguments to cabal install"+ , Menu.createEditableField (Common.MenuElement Common.IsolateEditBox) filepathV isolateDir+ & Menu.fieldLabelL .~ "isolated"+ & Menu.fieldHelpMsgL .~ "install in an isolated absolute directory instead of the default one"+ , Menu.createEditableField (Common.MenuElement Common.OvewrwiteVerEditBox) overWriteVersionParser overwriteVer+ & Menu.fieldLabelL .~ "overwrite version"+ & Menu.fieldHelpMsgL .~ "Allows to overwrite the finally installed VERSION with a different one"+ , Menu.createEditableField (Common.MenuElement Common.PatchesEditBox) patchesV patches+ & Menu.fieldLabelL .~ "patches"+ & Menu.fieldHelpMsgL .~ "Either a URI to a patch (https/http/file) or Absolute path to patch directory"+ , Menu.createEditableField (Common.MenuElement Common.CabalProjectEditBox) cabalProjectV cabalProject+ & Menu.fieldLabelL .~ "cabal project"+ & Menu.fieldHelpMsgL .~ "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."+ , Menu.createEditableField (Common.MenuElement Common.CabalProjectLocalEditBox) cabalProjectLocalV cabalProjectLocal+ & Menu.fieldLabelL .~ "cabal project local"+ & Menu.fieldHelpMsgL .~ "URI (https/http/file) to a cabal.project.local to be used for the build. Will be copied over."+ , Menu.createEditableField (Common.MenuElement Common.GitRefEditBox) (Right . Just . T.unpack) gitRef+ & Menu.fieldLabelL .~ "git-ref"+ & Menu.fieldHelpMsgL .~ "The git commit/branch/ref to build from"+ ]++ buttons = [+ Menu.createButtonField (Common.MenuElement Common.OkButton)+ & Menu.fieldLabelL .~ "Compile"+ & Menu.fieldHelpMsgL .~ "Compile HLS from source with options below"+ ]++handler :: BrickEvent Name e -> EventM Name CompileHLSMenu ()+handler = Menu.handlerMenu+++draw :: CompileHLSMenu -> Widget Name+draw = Common.frontwardLayer "Compile HLS" . Menu.drawMenu
+ lib-tui/GHCup/Brick/Widgets/Menus/Context.hs view
@@ -0,0 +1,83 @@+{-# LANGUAGE OverloadedStrings #-}++module GHCup.Brick.Widgets.Menus.Context (ContextMenu, create, draw, handler) where++import Brick (+ Widget (..), BrickEvent, EventM,+ )+import Data.Function ((&))+import Prelude hiding (appendFile)++import Data.Versions (prettyVer)+import GHCup.List ( ListResult(..) )+import GHCup.Types (KeyCombination, Tool (..))++import qualified GHCup.Brick.Common as Common+import qualified GHCup.Brick.Widgets.Menu as Menu+import GHCup.Brick.Common (Name (..))+import GHCup.Brick.Widgets.Menu (Menu)+import qualified Brick.Widgets.Core as Brick+import qualified Brick.Widgets.Border as Border+import qualified Brick.Focus as F+import Brick.Widgets.Core ((<+>))++import Optics (to)+import Optics.Operators ((.~), (^.))+import Optics.Optic ((%))+import Data.Foldable (foldl')++type ContextMenu = Menu ListResult Name++create :: ListResult -> KeyCombination -> ContextMenu+create lr exit_key = Menu.createMenu Common.ContextBox lr validator exit_key buttons []+ where+ advInstallButton =+ Menu.createButtonField (MenuElement Common.AdvanceInstallButton)+ & Menu.fieldLabelL .~ "Install"+ & Menu.fieldHelpMsgL .~ "Advance Installation Settings"+ compileGhcButton =+ Menu.createButtonField (MenuElement Common.CompileGHCButton)+ & Menu.fieldLabelL .~ "Compile"+ & Menu.fieldHelpMsgL .~ "Compile GHC from source"+ compileHLSButton =+ Menu.createButtonField (MenuElement Common.CompileHLSButton)+ & Menu.fieldLabelL .~ "Compile"+ & Menu.fieldHelpMsgL .~ "Compile HLS from source"+ buttons =+ case lTool lr of+ GHC -> [advInstallButton, compileGhcButton]+ HLS -> [advInstallButton, compileHLSButton]+ _ -> [advInstallButton]+ validator = const Nothing++draw :: ContextMenu -> Widget Name+draw menu =+ Common.frontwardLayer+ ("Context Menu for " <> tool_str <> " " <> prettyVer (lVer $ menu ^. Menu.menuStateL))+ $ Brick.vBox+ [ Brick.vBox buttonWidgets+ , Brick.txt " "+ , Brick.padRight Brick.Max $+ Brick.txt "Press "+ <+> Common.keyToWidget (menu ^. Menu.menuExitKeyL)+ <+> Brick.txt " to go back"+ ]+ where+ buttonLabels = [button & Menu.fieldLabel | button <- menu ^. Menu.menuButtonsL]+ maxWidth = foldl' max 5 (fmap Brick.textWidth buttonLabels)++ buttonAmplifiers =+ let buttonAsWidgets = fmap Menu.renderAslabel buttonLabels+ in fmap (\f b -> ((Menu.leftify (maxWidth + 10) . Border.border $ f b) <+>) ) buttonAsWidgets+ drawButtons = fmap Menu.drawField buttonAmplifiers+ buttonWidgets = zipWith (F.withFocusRing (menu ^. Menu.menuFocusRingL)) drawButtons (menu ^. Menu.menuButtonsL)+ tool_str =+ case menu ^. Menu.menuStateL % to lTool of+ GHC -> "GHC"+ GHCup -> "GHCup"+ Cabal -> "Cabal"+ HLS -> "HLS"+ Stack -> "Stack"++handler :: BrickEvent Name e -> EventM Name ContextMenu ()+handler = Menu.handlerMenu
@@ -0,0 +1,149 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RankNTypes #-}+{-# OPTIONS_GHC -Wno-unused-record-wildcards #-}+{-# OPTIONS_GHC -Wno-unused-matches #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}++{- Brick's navigation widget:+It is a FocusRing over many list's. Each list contains the information for each tool. Each list has an internal name (for Brick's runtime)+and a label which we can use in rendering. This data-structure helps to reuse Brick.Widget.List and to navegate easily across++-}+++module GHCup.Brick.Widgets.Navigation (BrickInternalState, create, handler, draw) where++import GHCup.List ( ListResult(..) )+import GHCup.Types+ ( GHCTargetVersion(GHCTargetVersion),+ Tool(..),+ Tag(..),+ tVerToText,+ tagToString )+import qualified GHCup.Brick.Common as Common+import qualified GHCup.Brick.Attributes as Attributes+import qualified GHCup.Brick.Widgets.SectionList as SectionList+import Brick+ ( BrickEvent(..),+ Padding(Max, Pad),+ AttrMap,+ EventM,+ Widget(..),+ (<+>),+ (<=>))+import qualified Brick+import Brick.Widgets.Border ( hBorder, borderWithLabel)+import Brick.Widgets.Border.Style ( unicode )+import Brick.Widgets.Center ( center )+import qualified Brick.Widgets.List as L+import Data.List ( intercalate, sort )+import Data.Maybe ( mapMaybe )+import Data.Vector ( Vector)+import Data.Versions ( prettyPVP, prettyVer )+import Prelude hiding ( appendFile )+import qualified Data.Text as T+import qualified Data.Vector as V+++type BrickInternalState = SectionList.SectionList Common.Name ListResult++-- | How to create a navigation widget+create :: Common.Name -- The name of the section list+ -> [(Common.Name, Vector ListResult)] -- a list of tuples (section name, collection of elements)+ -> Int -- The height of each item in a list. Commonly 1+ -> BrickInternalState+create = SectionList.sectionList++-- | How the navigation handler handle events+handler :: BrickEvent Common.Name e -> EventM Common.Name BrickInternalState ()+handler = SectionList.handleGenericListEvent++-- | How to draw the navigation widget+draw :: AttrMap -> BrickInternalState -> Widget Common.Name+draw dimAttrs section_list+ = Brick.padBottom Max+ ( Brick.withBorderStyle unicode+ $ borderWithLabel (Brick.str "GHCup")+ (center (header <=> hBorder <=> renderList' section_list))+ )+ where+ header =+ minHSize 2 Brick.emptyWidget+ <+> Brick.padLeft (Pad 2) (minHSize 6 $ Brick.str "Tool")+ <+> minHSize 15 (Brick.str "Version")+ <+> Brick.padLeft (Pad 1) (minHSize 25 $ Brick.str "Tags")+ <+> Brick.padLeft (Pad 5) (Brick.str "Notes")+ renderList' bis =+ let allElements = V.concatMap L.listElements $ SectionList.sectionListElements bis+ minTagSize = V.maximum $ V.map (length . intercalate "," . fmap tagToString . lTag) allElements+ minVerSize = V.maximum $ V.map (\ListResult{..} -> T.length $ tVerToText (GHCTargetVersion lCross lVer)) allElements+ in Brick.withDefAttr L.listAttr $ SectionList.renderSectionList (renderItem minTagSize minVerSize) True bis+ renderItem minTagSize minVerSize b listResult@ListResult{lTag = lTag', ..} =+ let marks = if+ | lSet -> (Brick.withAttr Attributes.setAttr $ Brick.str Common.setSign)+ | lInstalled -> (Brick.withAttr Attributes.installedAttr $ Brick.str Common.installedSign)+ | otherwise -> (Brick.withAttr Attributes.notInstalledAttr $ Brick.str Common.notInstalledSign)+ ver = case lCross of+ Nothing -> T.unpack . prettyVer $ lVer+ Just c -> T.unpack (c <> "-" <> prettyVer lVer)+ dim+ | lNoBindist && not lInstalled+ && not b -- TODO: overloading dim and active ignores active+ -- so we hack around it here+ = Brick.updateAttrMap (const dimAttrs) . Brick.withAttr (Brick.attrName "no-bindist")+ | otherwise = id+ hooray+ | elem Latest lTag' && not lInstalled =+ Brick.withAttr Attributes.hoorayAttr+ | otherwise = id+ active = if b then Common.enableScreenReader Common.AllTools else id+ in hooray $ active $ dim+ ( marks+ <+> Brick.padLeft (Pad 2)+ ( minHSize 6+ (printTool lTool)+ )+ <+> minHSize minVerSize (Brick.str ver)+ <+> (let l = mapMaybe printTag $ sort lTag'+ in Brick.padLeft (Pad 1) $ minHSize minTagSize $ if null l+ then Brick.emptyWidget+ else foldr1 (\x y -> x <+> Brick.str "," <+> y) l+ )+ <+> Brick.padLeft (Pad 5)+ ( let notes = printNotes listResult+ in if null notes+ then Brick.emptyWidget+ else foldr1 (\x y -> x <+> Brick.str "," <+> y) notes+ )+ <+> Brick.vLimit 1 (Brick.fill ' ')+ )++ printTag Recommended = Just $ Brick.withAttr Attributes.recommendedAttr $ Brick.str "recommended"+ printTag Latest = Just $ Brick.withAttr Attributes.latestAttr $ Brick.str "latest"+ printTag Prerelease = Just $ Brick.withAttr Attributes.prereleaseAttr $ Brick.str "prerelease"+ printTag Nightly = Just $ Brick.withAttr Attributes.nightlyAttr $ Brick.str "nightly"+ printTag (Base pvp'') = Just $ Brick.str ("base-" ++ T.unpack (prettyPVP pvp''))+ printTag Old = Nothing+ printTag LatestPrerelease = Just $ Brick.withAttr Attributes.latestPrereleaseAttr $ Brick.str "latest-prerelease"+ printTag LatestNightly = Just $ Brick.withAttr Attributes.latestNightlyAttr $ Brick.str "latest-nightly"+ printTag (UnknownTag t) = Just $ Brick.str t++ printTool Cabal = Brick.str "cabal"+ printTool GHC = Brick.str "GHC"+ printTool GHCup = Brick.str "GHCup"+ printTool HLS = Brick.str "HLS"+ printTool Stack = Brick.str "Stack"++ printNotes ListResult {..} =+ (if hlsPowered then [Brick.withAttr Attributes.hlsPoweredAttr $ Brick.str "hls-powered"] else mempty+ )+ ++ (if lStray then [Brick.withAttr Attributes.strayAttr $ Brick.str "stray"] else mempty)+ ++ (case lReleaseDay of+ Nothing -> mempty+ Just d -> [Brick.withAttr Attributes.dayAttr $ Brick.str (show d)])++ minHSize s' = Brick.hLimit s' . Brick.vLimit 1 . (<+> Brick.fill ' ')
+ lib-tui/GHCup/Brick/Widgets/SectionList.hs view
@@ -0,0 +1,193 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE ViewPatterns #-}+{-# OPTIONS_GHC -Wno-unused-record-wildcards #-}+{-# OPTIONS_GHC -Wno-unused-matches #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE InstanceSigs #-}++{- A general system for lists with sections++Consider this code as private. GenericSectionList should not be used directly as the FocusRing should be align with the Vector containing+the elements, otherwise you'd be focusing on a non-existent widget with unknown result (In theory the code is safe unless you have an empty section list).++- To build a SectionList use the safe constructor sectionList+- To access sections use the lens provider sectionL and the name of the section you'd like to access+- You can modify Brick.Widget.List.GenericList within GenericSectionList via sectionL but do not+ modify the vector length++-}+++module GHCup.Brick.Widgets.SectionList where+++import Brick+ ( BrickEvent(VtyEvent, MouseDown),+ EventM,+ Size(..),+ Widget(..),+ ViewportType (Vertical),+ (<=>))+import qualified Brick+import Brick.Widgets.Border ( hBorder)+import qualified Brick.Widgets.List as L+import Brick.Focus (FocusRing)+import qualified Brick.Focus as F+import Data.Function ( (&))+import Data.Maybe ( fromMaybe )+import Data.Vector ( Vector )+import qualified GHCup.Brick.Common as Common+import Prelude hiding ( appendFile )++import qualified Graphics.Vty as Vty+import qualified Data.Vector as V++import Optics.TH (makeLensesFor)+import Optics.State (use)+import Optics.State.Operators ( (%=), (<%=))+import Optics.Operators ((.~), (^.))+import Optics.Lens (Lens', lens)++data GenericSectionList n t e+ = GenericSectionList+ { sectionListFocusRing :: FocusRing n -- ^ The FocusRing for all sections+ , sectionListElements :: !(Vector (L.GenericList n t e)) -- ^ A vector of brick's built-in list+ , sectionListName :: n -- ^ The section list name+ }++makeLensesFor [("sectionListFocusRing", "sectionListFocusRingL"), ("sectionListElements", "sectionListElementsL"), ("sectionListName", "sectionListNameL")] ''GenericSectionList++type SectionList n e = GenericSectionList n V.Vector e+++-- | Build a SectionList from nonempty list. If empty we could not defined sectionL lenses.+sectionList :: Foldable t+ => n -- The name of the section list+ -> [(n, t e)] -- a list of tuples (section name, collection of elements)+ -> Int+ -> GenericSectionList n t e+sectionList name elements height+ = GenericSectionList+ { sectionListFocusRing = F.focusRing [section_name | (section_name, _) <- elements]+ , sectionListElements = V.fromList [L.list section_name els height | (section_name, els) <- elements]+ , sectionListName = name+ }+-- | This lens constructor, takes a name and looks if a section has such a name.+-- Used to dispatch events to sections. It is a partial function only meant to+-- be used with the FocusRing inside GenericSectionList+sectionL :: Eq n => n -> Lens' (GenericSectionList n t e) (L.GenericList n t e)+sectionL section_name = lens g s+ where is_section_name = (== section_name) . L.listName+ g section_list =+ let elms = section_list ^. sectionListElementsL+ zeroth = elms V.! 0 -- TODO: This crashes for empty vectors.+ in fromMaybe zeroth (V.find is_section_name elms)+ s gl@(GenericSectionList _ elms _) list =+ case V.findIndex is_section_name elms of+ Nothing -> gl+ Just i -> let new_elms = V.update elms (V.fromList [(i, list)])+ in gl & sectionListElementsL .~ new_elms++moveDown :: (L.Splittable t, Ord n, Foldable t) => EventM n (GenericSectionList n t e) ()+moveDown = do+ ring <- use sectionListFocusRingL+ case F.focusGetCurrent ring of+ Nothing -> pure ()+ Just l -> do -- If it is the last element, move to the first element of the next focus; else, just handle regular list event.+ current_list <- use (sectionL l)+ let current_idx = L.listSelected current_list+ list_length = current_list & length+ if current_idx == Just (list_length - 1)+ then do+ new_focus <- sectionListFocusRingL <%= F.focusNext+ case F.focusGetCurrent new_focus of+ Nothing -> pure () -- |- Optic.Zoom.zoom doesn't typecheck but Lens.Micro.Mtl.zoom does. It is re-exported by Brick+ Just new_l -> Common.zoom (sectionL new_l) (Brick.modify L.listMoveToBeginning)+ else Common.zoom (sectionL l) $ Brick.modify L.listMoveDown++moveUp :: (L.Splittable t, Ord n, Foldable t) => EventM n (GenericSectionList n t e) ()+moveUp = do+ ring <- use sectionListFocusRingL+ case F.focusGetCurrent ring of+ Nothing -> pure ()+ Just l -> do -- If it is the first element, move to the last element of the prev focus; else, just handle regular list event.+ current_list <- use (sectionL l)+ let current_idx = L.listSelected current_list+ if current_idx == Just 0+ then do+ new_focus <- sectionListFocusRingL <%= F.focusPrev+ case F.focusGetCurrent new_focus of+ Nothing -> pure ()+ Just new_l -> Common.zoom (sectionL new_l) (Brick.modify L.listMoveToEnd)+ else Common.zoom (sectionL l) $ Brick.modify L.listMoveUp++-- | Handle events for list cursor movement. Events handled are:+--+-- * Up (up arrow key). If first element of section, then jump prev section+-- * Down (down arrow key). If last element of section, then jump next section+-- * Page Up (PgUp)+-- * Page Down (PgDown)+-- * Go to next section (Tab)+-- * Go to prev section (BackTab)+handleGenericListEvent :: (Foldable t, L.Splittable t, Ord n)+ => BrickEvent n a+ -> EventM n (GenericSectionList n t e) ()+handleGenericListEvent (VtyEvent (Vty.EvResize _ _)) = pure ()+handleGenericListEvent (VtyEvent (Vty.EvKey (Vty.KChar '\t') [])) = sectionListFocusRingL %= F.focusNext+handleGenericListEvent (VtyEvent (Vty.EvKey Vty.KBackTab [])) = sectionListFocusRingL %= F.focusPrev+handleGenericListEvent (MouseDown _ Vty.BScrollDown _ _) = moveDown+handleGenericListEvent (MouseDown _ Vty.BScrollUp _ _) = moveUp+handleGenericListEvent (VtyEvent (Vty.EvKey Vty.KDown [])) = moveDown+handleGenericListEvent (VtyEvent (Vty.EvKey Vty.KUp [])) = moveUp+handleGenericListEvent (VtyEvent ev) = do+ ring <- use sectionListFocusRingL+ case F.focusGetCurrent ring of+ Nothing -> pure ()+ Just l -> Common.zoom (sectionL l) $ L.handleListEvent ev+handleGenericListEvent _ = pure ()++-- This re-uses Brick.Widget.List.renderList+renderSectionList :: forall n t e . (Traversable t, Ord n, Show n, Eq n, L.Splittable t, Semigroup (t e))+ => (Bool -> e -> Widget n) -- ^ Rendering function of the list element, True for the selected element+ -> Bool -- ^ Whether the section list has focus+ -> GenericSectionList n t e -- ^ The section list to render+ -> Widget n+renderSectionList renderElem sectionFocus ge@(GenericSectionList focus elms slName) =+ Brick.Widget Brick.Greedy Brick.Greedy $ Brick.render $ Brick.viewport slName Brick.Vertical $+ V.ifoldl' (\(!accWidget) !i list ->+ let hasFocusList = sectionIsFocused list+ makeVisible = if hasFocusList then Brick.visibleRegion (Brick.Location (c, r)) (1, 1) else id+ appendBorder = if i == 0 then id else (hBorder <=>)+ newWidget = appendBorder (makeVisible $ renderInnerList hasFocusList list)+ in accWidget <=> newWidget+ )+ Brick.emptyWidget+ elms+ where+ -- A section is focused if the whole thing is focused, and the inner list has focus+ sectionIsFocused :: L.GenericList n t e -> Bool+ sectionIsFocused l = sectionFocus && (Just (L.listName l) == F.focusGetCurrent focus)++ renderInnerList :: Bool -> L.GenericList n t e -> Widget n+ renderInnerList hasFocus l = Brick.vLimit (length l) $ L.renderList (\b -> renderElem (b && hasFocus)) hasFocus l++ -- compute the location to focus on within the active section+ (c, r) :: (Int, Int) = case sectionListSelectedElement ge of+ Nothing -> (0, 0)+ Just (selElIx, _) -> (0, selElIx)+++-- | Equivalent to listSelectedElement+sectionListSelectedElement :: (Eq n, L.Splittable t, Traversable t, Semigroup (t e)) => GenericSectionList n t e -> Maybe (Int, e)+sectionListSelectedElement generic_section_list = do+ current_focus <- generic_section_list ^. sectionListFocusRingL & F.focusGetCurrent+ let current_section = generic_section_list ^. sectionL current_focus+ L.listSelectedElement current_section
+ lib-tui/GHCup/Brick/Widgets/Tutorial.hs view
@@ -0,0 +1,77 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RankNTypes #-}+{-# OPTIONS_GHC -Wno-unused-record-wildcards #-}+{-# OPTIONS_GHC -Wno-unused-matches #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}++{-+A very simple information-only widget with no handler.+-}++module GHCup.Brick.Widgets.Tutorial (draw) where++import qualified GHCup.Brick.Common as Common+import qualified GHCup.Brick.Attributes as Attributes++import Brick+ ( Padding(Max),+ Widget(..),+ (<=>))+import qualified Brick+import Brick.Widgets.Center ( center )+import Prelude hiding ( appendFile )++++draw :: Widget Common.Name+draw =+ let+ mkTextBox = Brick.hLimitPercent 70 . Brick.vBox . fmap (Brick.padRight Brick.Max)++ in Common.frontwardLayer "Tutorial"+ $ Brick.vBox+ (fmap center+ [ mkTextBox [Brick.txtWrap "GHCup is a distribution channel for Haskell's tools."]+ , Common.separator+ , mkTextBox [+ Brick.hBox [+ Brick.txt "This symbol "+ , Brick.withAttr Attributes.installedAttr (Brick.str Common.installedSign)+ , Brick.txtWrap " means that the tool is installed but not in used"+ ]+ , Brick.hBox [+ Brick.txt "This symbol "+ , Brick.withAttr Attributes.setAttr (Brick.str Common.setSign)+ , Brick.txtWrap " means that the tool is installed and in used"+ ]+ , Brick.hBox [+ Brick.txt "This symbol "+ , Brick.withAttr Attributes.notInstalledAttr (Brick.str Common.notInstalledSign)+ , Brick.txt " means that the tool isn't installed"+ ]+ ]+ , Common.separator+ , mkTextBox [+ Brick.hBox [+ Brick.withAttr Attributes.recommendedAttr $ Brick.str "recommended"+ , Brick.txtWrap " tag is based on community adoption, known bugs, etc... So It makes this version the least experimental"+ ]+ , Brick.hBox [+ Brick.withAttr Attributes.latestAttr $ Brick.str "latest"+ , Brick.txtWrap " tag is for the latest distributed version of the tool"+ ]+ , Brick.hBox [+ Brick.withAttr Attributes.latestAttr $ Brick.str "hls-powered"+ , Brick.txt " denotes the compiler version supported by the currently set ("+ , Brick.withAttr Attributes.setAttr (Brick.str Common.setSign)+ , Brick.txt ") hls"+ ]+ , Brick.txtWrap "base-X.Y.Z.W tag is the minimun version of the base package admited in such ghc version"+ ]+ , Brick.txt " "+ ])+ <=> Brick.padRight Brick.Max (Brick.txt "Press c+ctrl to exit the tutorial")
+ lib-tui/GHCup/BrickMain.hs view
@@ -0,0 +1,79 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RankNTypes #-}+{-# OPTIONS_GHC -Wno-unused-record-wildcards #-}+{-# OPTIONS_GHC -Wno-unused-matches #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}++{-+This module contains the entrypoint for the brick application and nothing else.++-}++module GHCup.BrickMain where++import GHCup.Types+ ( Settings(noColor),+ AppState(ghcupInfo, settings, keyBindings, loggerConfig), KeyCombination (KeyCombination) )+import GHCup.Prelude.Logger ( logError )+import qualified GHCup.Brick.Actions as Actions+import qualified GHCup.Brick.Common as Common+import qualified GHCup.Brick.App as BrickApp+import qualified GHCup.Brick.Attributes as Attributes+import qualified GHCup.Brick.BrickState as AppState+import qualified GHCup.Brick.Widgets.Menus.Context as ContextMenu+import qualified GHCup.Brick.Widgets.SectionList as Navigation+import qualified GHCup.Brick.Widgets.Menus.AdvanceInstall as AdvanceInstall+import qualified GHCup.Brick.Widgets.Menus.CompileGHC as CompileGHC+import qualified Brick+import qualified Graphics.Vty as Vty++import Control.Monad.Reader ( ReaderT(runReaderT) )+import Data.Functor ( ($>) )+import Data.IORef (writeIORef)+import Prelude hiding ( appendFile )+import System.Exit ( ExitCode(ExitFailure), exitWith )++import qualified Data.Text as T+import qualified GHCup.Brick.Widgets.Menus.CompileHLS as CompileHLS++++brickMain :: AppState+ -> IO ()+brickMain s = do+ writeIORef Actions.settings' s++ eAppData <- Actions.getAppData (Just $ ghcupInfo s)+ case eAppData of+ Right ad -> do+ let initial_list = Actions.constructList ad Common.defaultAppSettings Nothing+ current_element = Navigation.sectionListSelectedElement initial_list+ exit_key = KeyCombination (Vty.KChar 'c') [Vty.MCtrl] -- bQuit . keyBindings $ s+ case current_element of+ Nothing -> do+ flip runReaderT s $ logError "Error building app state: empty ResultList"+ exitWith $ ExitFailure 2+ Just (_, e) ->+ let initapp =+ BrickApp.app+ (Attributes.defaultAttributes $ noColor $ settings s)+ (Attributes.dimAttributes $ noColor $ settings s)+ initstate =+ AppState.BrickState ad+ Common.defaultAppSettings+ initial_list+ (ContextMenu.create e exit_key)+ (AdvanceInstall.create exit_key)+ (CompileGHC.create exit_key)+ (CompileHLS.create exit_key)+ (keyBindings s)+ Common.Navigation+ in Brick.defaultMain initapp initstate+ $> ()+ Left e -> do+ flip runReaderT s $ logError $ "Error building app state: " <> T.pack (show e)+ exitWith $ ExitFailure 2
lib/GHCup.hs view
@@ -50,6 +50,7 @@ import GHCup.Prelude.String.QQ import GHCup.Version +import Conduit (sourceToList) import Control.Applicative import Control.Exception.Safe import Control.Monad@@ -77,7 +78,6 @@ import Text.Regex.Posix import qualified Data.Text as T-import qualified Streamly.Prelude as S @@ -157,6 +157,7 @@ rmGhcupDirs :: ( MonadReader env m , HasDirs env , MonadIO m+ , MonadUnliftIO m , HasLog env , MonadCatch m , MonadMask m )@@ -220,9 +221,9 @@ when (not isXDGStyle) $ removeDirIfEmptyOrIsSymlink binDir - reportRemainingFiles :: (MonadMask m, MonadIO m) => FilePath -> m [FilePath]+ reportRemainingFiles :: (MonadMask m, MonadIO m, MonadUnliftIO m) => FilePath -> m [FilePath] reportRemainingFiles dir = do- remainingFiles <- liftIO $ S.toList (getDirectoryContentsRecursiveUnsafe dir)+ remainingFiles <- runResourceT $ sourceToList $ getDirectoryContentsRecursiveUnsafe dir let normalizedFilePaths = fmap normalise remainingFiles let sortedByDepthRemainingFiles = sortBy (flip compareFn) normalizedFilePaths let remainingFilesAbsolute = fmap (dir </>) sortedByDepthRemainingFiles
+ lib/GHCup/CabalConfig.hs view
@@ -0,0 +1,111 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE UndecidableInstances #-}++module GHCup.CabalConfig (getStoreDir) where++import Data.ByteString (ByteString)+import Data.List.NonEmpty (NonEmpty)+import Data.Map (Map)+import System.Directory (getAppUserDataDirectory, doesDirectoryExist, getXdgDirectory, XdgDirectory(XdgConfig))+import System.Environment (lookupEnv)+import System.FilePath ((</>))++import qualified Data.ByteString as BS+import qualified Data.Map.Strict as M+import qualified Distribution.CabalSpecVersion as C+import qualified Distribution.FieldGrammar as C+import qualified Distribution.FieldGrammar.Parsec as C+import qualified Distribution.Fields as C+import qualified Distribution.Fields.LexerMonad as C+import qualified Distribution.Parsec as C+import qualified Distribution.Utils.Generic as C+import qualified Text.Parsec as P++import Data.Foldable (for_)+import Distribution.Parsec.Error+++++getStoreDir :: IO FilePath+getStoreDir = do+ fp <- findConfig+ bs <- BS.readFile fp+ either (fail . show . fmap (showPError fp)) resolveConfig (parseConfig bs)++-------------------------------------------------------------------------------+-- Find config+-------------------------------------------------------------------------------++-- | Find the @~\/.cabal\/config@ file.+findConfig :: IO FilePath+findConfig = do+ env <- lookupEnv "CABAL_CONFIG"+ case env of+ Just p -> return p+ Nothing -> do+ cabalDir <- findCabalDir+ return (cabalDir </> "config")++-- | Find the @~\/.cabal@ dir.+findCabalDir :: IO FilePath+findCabalDir = do+ cabalDirVar <- lookupEnv "CABAL_DIR"+ appDir <- getAppUserDataDirectory "cabal"+ isXdg <- not <$> doesDirectoryExist appDir+ if | Just dir <- cabalDirVar -> pure dir+ | isXdg -> getXdgDirectory XdgConfig "cabal"+ | otherwise -> pure appDir+++-------------------------------------------------------------------------------+-- Parsing+-------------------------------------------------------------------------------++-- | Parse @~\/.cabal\/config@ file.+parseConfig :: ByteString -> Either (NonEmpty PError) (Maybe FilePath)+parseConfig = parseWith $ \fields0 -> do+ let (fields1, _) = C.partitionFields fields0+ let fields2 = M.filterWithKey (\k _ -> k `elem` knownFields) fields1+ parse fields2+ where+ knownFields = C.fieldGrammarKnownFieldList grammar++ parse :: Map C.FieldName [C.NamelessField C.Position]+ -> C.ParseResult (Maybe FilePath)+ parse fields = C.parseFieldGrammar C.cabalSpecLatest fields grammar++grammar :: C.ParsecFieldGrammar (Maybe FilePath) (Maybe FilePath)+grammar = mempty+ <$> C.optionalFieldAla "store-dir" C.FilePathNT id++parseWith+ :: ([C.Field C.Position] -> C.ParseResult a) -- ^ parse+ -> ByteString -- ^ contents+ -> Either (NonEmpty PError) a+parseWith parser bs = case C.runParseResult result of+ (_, Right x) -> Right x+ (_, Left (_, es)) -> Left es+ where+ result = case C.readFields' bs of+ Left perr -> C.parseFatalFailure pos (show perr) where+ ppos = P.errorPos perr+ pos = C.Position (P.sourceLine ppos) (P.sourceColumn ppos)+ Right (fields, lexWarnings) -> do+ C.parseWarnings (C.toPWarnings lexWarnings)+ for_ (C.validateUTF8 bs) $ \pos ->+ C.parseWarning C.zeroPos C.PWTUTF $ "UTF8 encoding problem at byte offset " ++ show pos+ parser fields++-------------------------------------------------------------------------------+-- Resolving+-------------------------------------------------------------------------------++-- | Fill the default in @~\/.cabal\/config@ file.+resolveConfig :: Maybe FilePath -> IO FilePath+resolveConfig (Just fp) = pure fp+resolveConfig Nothing = do+ c <- findCabalDir+ return (c </> "store")+
lib/GHCup/Download/IOStreams.hs view
@@ -123,11 +123,16 @@ downloadStream r i' = do void setup- let size = case getHeader r "Content-Length" of- Just x' -> case decimal $ decUTF8Safe x' of+ -- We can only do content length checks & progress bar when the stream is not encoded+ let notEncoded = case mk <$> getHeader r "Content-Encoding" of+ Nothing -> True+ Just "identity" -> True+ Just _ -> False+ size = case (notEncoded, getHeader r "Content-Length") of+ (True, Just x') -> case decimal $ decUTF8Safe x' of Left _ -> Nothing Right (r', _) -> Just r'- Nothing -> Nothing+ _ -> Nothing forM_ size $ \s -> forM_ eCSize $ \es -> when (es /= s) $ throwIO (ContentLengthError Nothing (Just s) es) let size' = eCSize <|> size
lib/GHCup/GHC.hs view
@@ -439,39 +439,22 @@ liftE $ mergeGHCFileTree path inst tver forceInstall | otherwise = do PlatformRequest {..} <- lift getPlatformReq+ Settings {..} <- lift getSettings - let ldOverride- | _tvVersion tver >= [vver|8.2.2|]- , _rPlatform `elem` [Linux Alpine, Darwin]- = ["--disable-ld-override"]- | otherwise- = []+ addConfArgs' <- sanitizefGHCconfOptions (T.unpack <$> addConfArgs)+ defGHCConfOptions' <- sanitizefGHCconfOptions defGHCConfOptions lift $ logInfo "Installing GHC (this may take a while)"- env <- case _rPlatform of- -- https://github.com/haskell/ghcup-hs/issues/967- Linux Alpine- -- lets not touch LD for cross targets- | Nothing <- _tvTarget tver -> do- cEnv <- liftIO getEnvironment- spaths <- liftIO getSearchPath- has_ld_bfd <- isJust <$> liftIO (searchPath spaths "ld.bfd")- let ldSet = isJust $ lookup "LD" cEnv- -- only set LD if ld.bfd exists in PATH and LD is not set- -- already- if has_ld_bfd && not ldSet- then do- lift $ logInfo "Detected alpine linux... setting LD=ld.bfd"- pure $ Just (("LD", "ld.bfd") : cEnv)- else pure Nothing- _ -> pure Nothing lEM $ execLogged "sh" ("./configure" : ("--prefix=" <> fromInstallDir inst)- : (maybe mempty (\x -> ["--target=" <> T.unpack x]) (_tvTarget tver) <> ldOverride <> (T.unpack <$> addConfArgs))+ : (maybe mempty (\x -> ["--target=" <> T.unpack x]) (_tvTarget tver)+ <> ldOverride (_tvVersion tver) _rPlatform+ <> defGHCConfOptions'+ <> addConfArgs') ) (Just $ fromGHCupPath path) "ghc-configure"- env+ Nothing tmpInstallDest <- lift withGHCupTmpDir lEM $ make ["DESTDIR=" <> fromGHCupPath tmpInstallDest, "install"] (Just $ fromGHCupPath path) liftE $ catchWarn $ lEM @_ @'[ProcessError] $ darwinNotarization _rPlatform (fromGHCupPath tmpInstallDest)@@ -479,6 +462,7 @@ pure () + mergeGHCFileTree :: ( MonadReader env m , HasPlatformReq env , HasDirs env@@ -808,7 +792,8 @@ => GHCVer -> Maybe Text -- ^ cross target -> Maybe [VersionPattern]- -> Either Version FilePath -- ^ version to bootstrap with+ -> Either Version FilePath -- ^ GHC version to bootstrap with+ -> Maybe (Either Version FilePath) -- ^ GHC version to compile hadrian with -> Maybe Int -- ^ jobs -> Maybe FilePath -- ^ build config -> Maybe (Either FilePath [URI]) -- ^ patches@@ -843,7 +828,7 @@ ] m GHCTargetVersion-compileGHC targetGhc crossTarget vps bstrap jobs mbuildConfig patches aargs buildFlavour buildSystem installDir+compileGHC targetGhc crossTarget vps bstrap hghc jobs mbuildConfig patches aargs buildFlavour buildSystem installDir = do pfreq@PlatformRequest { .. } <- lift getPlatformReq GHCupInfo { _ghcupDownloads = dls } <- lift getGHCupInfo@@ -892,7 +877,7 @@ execBlank regex )- tver <- liftE $ catchAllE @_ @'[ProcessError, ParseError] @'[] (\_ -> pure Nothing) $ fmap Just $ getGHCVer+ tver <- liftE $ catchAllE @_ @'[ProcessError, ParseError, NotFoundInPATH] @'[] (\_ -> pure Nothing) $ fmap Just $ getGHCVer (appendGHCupPath tmpUnpack (takeDirectory bootFile)) pure (bootFile, tver) @@ -949,7 +934,7 @@ liftE $ applyAnyPatch patches (fromGHCupPath tmpUnpack) -- bootstrap- tver <- liftE $ catchAllE @_ @'[ProcessError, ParseError] @'[] (\_ -> pure Nothing) $ fmap Just $ getGHCVer+ tver <- liftE $ catchAllE @_ @'[ProcessError, ParseError, NotFoundInPATH] @'[] (\_ -> pure Nothing) $ fmap Just $ getGHCVer tmpUnpack liftE $ catchWarn $ lEM @_ @'[ProcessError] $ darwinNotarization _rPlatform (fromGHCupPath tmpUnpack) lift $ logInfo $ "Examining git ref " <> T.pack ref <> "\n " <>@@ -1058,10 +1043,10 @@ , MonadThrow m ) => GHCupPath- -> Excepts '[ProcessError, ParseError] m Version+ -> Excepts '[ProcessError, ParseError, NotFoundInPATH] m Version getGHCVer tmpUnpack = do lEM $ execLogged "python3" ["./boot"] (Just $ fromGHCupPath tmpUnpack) "ghc-bootstrap" Nothing- lEM $ configureWithGhcBoot Nothing [] (Just $ fromGHCupPath tmpUnpack) "ghc-bootstrap"+ liftE $ configureWithGhcBoot Nothing [] (Just $ fromGHCupPath tmpUnpack) "ghc-bootstrap" let versionFile = fromGHCupPath tmpUnpack </> "VERSION" hasVersionFile <- liftIO $ doesFileExist versionFile if hasVersionFile@@ -1112,13 +1097,21 @@ lift $ logInfo $ "Building GHC version " <> tVerToText tver <> " (this may take a while)..." hadrian_build <- liftE $ findHadrianFile workdir+ hEnv <- case hghc of+ Nothing -> pure Nothing+ Just hghc' -> do+ cEnv <- Map.fromList <$> liftIO getEnvironment+ ghc <- liftE $ resolveGHC hghc'+ pure . Just . Map.toList . Map.insert "GHC" ghc $ cEnv++ lEM $ execLogged hadrian_build ( maybe [] (\j -> ["-j" <> show j] ) jobs ++ maybe [] (\bf -> ["--flavour=" <> bf]) buildFlavour ++ ["binary-dist"] ) (Just workdir) "ghc-make"- Nothing+ hEnv [tar] <- liftIO $ findFiles (workdir </> "_build" </> "bindist") (makeRegexOpts compExtended@@ -1306,13 +1299,16 @@ m () configureBindist tver workdir (fromInstallDir -> ghcdir) = do+ PlatformRequest { .. } <- lift getPlatformReq lift $ logInfo [s|configuring build|]- lEM $ configureWithGhcBoot (Just tver)+ liftE $ configureWithGhcBoot (Just tver) (maybe mempty (\x -> ["--target=" <> T.unpack x]) (_tvTarget tver) ++ ["--prefix=" <> ghcdir] ++ (if isWindows then ["--enable-tarballs-autodownload"] else [])+ -- https://github.com/haskell/ghcup-hs/issues/1032+ ++ ldOverride (_tvVersion tver) _rPlatform ++ fmap T.unpack aargs ) (Just workdir)@@ -1329,8 +1325,9 @@ -> [String] -- ^ args for configure -> Maybe FilePath -- ^ optionally chdir into this -> FilePath -- ^ log filename (opened in append mode)- -> m (Either ProcessError ())+ -> Excepts '[ProcessError, NotFoundInPATH] m () configureWithGhcBoot mtver args dir logf = do+ bghc <- liftE $ resolveGHC bstrap let execNew = execLogged "sh" ("./configure" : ("GHC=" <> bghc) : args)@@ -1344,13 +1341,17 @@ logf Nothing if | Just tver <- mtver- , _tvVersion tver >= [vver|8.8.0|] -> execNew- | Nothing <- mtver -> execNew -- need some default for git checkouts where we don't know yet- | otherwise -> execOld+ , _tvVersion tver >= [vver|8.8.0|] -> lEM execNew+ | Nothing <- mtver -> lEM execNew -- need some default for git checkouts where we don't know yet+ | otherwise -> lEM execOld - bghc = case bstrap of- Right g -> g- Left bver -> "ghc-" <> (T.unpack . prettyVer $ bver) <> exeExt+ resolveGHC :: MonadIO m => Either Version FilePath -> Excepts '[NotFoundInPATH] m FilePath+ resolveGHC = \case+ Right g -> pure g+ Left bver -> do+ let ghc = "ghc-" <> (T.unpack . prettyVer $ bver) <> exeExt+ -- https://gitlab.haskell.org/ghc/ghc/-/issues/24682+ makeAbsolute ghc @@ -1386,4 +1387,18 @@ $ getMajorMinorV _tvVersion forM_ v' $ \(mj, mi) -> lift (getGHCForPVP (PVP (fromIntegral mj :| [fromIntegral mi])) _tvTarget) >>= mapM_ (\v -> liftE $ setGHC v SetGHC_XY Nothing)+++ldOverride :: Version -> Platform -> [String]+ldOverride ver plat+ | ver >= [vver|8.2.2|]+ , plat `elem` [Linux Alpine, Darwin]+ = ["--disable-ld-override"]+ | otherwise+ = []++sanitizefGHCconfOptions :: MonadFail m => [String] -> m [String]+sanitizefGHCconfOptions args+ | "--prefix" `elem` fmap (takeWhile (/= '=')) args = fail "Don't explicitly set --prefix ...aborting"+ | otherwise = pure args
+ lib/GHCup/PlanJson.hs view
@@ -0,0 +1,79 @@+module GHCup.PlanJson where++import Control.Monad (unless)+import System.FilePath+import System.Directory++findPlanJson+ :: FilePath+ -> IO FilePath+findPlanJson fp = do+ planJsonFn <- do+ mRoot <- findProjectRoot fp+ case mRoot of+ Nothing -> fail ("missing project root relative to: " ++ fp)+ Just dir -> fromBuilddir $ dir </> "dist-newstyle"++ havePlanJson <- doesFileExist planJsonFn++ unless havePlanJson $+ fail "missing 'plan.json' file; do you need to run 'cabal new-build'?"++ return planJsonFn+ where+ fromBuilddir distFolder = do+ haveDistFolder <- doesDirectoryExist distFolder++ unless haveDistFolder $+ fail ("missing " ++ show distFolder ++ " folder; do you need to run 'cabal new-build'?")++ return $ distFolder </> "cache" </> "plan.json"+++-- | Find project root relative to a directory, this emulates cabal's current+-- heuristic, but is slightly more liberal. If no cabal.project is found,+-- cabal-install looks for *.cabal files in the specified directory only. This+-- function also considers *.cabal files in directories higher up in the+-- hierarchy.+findProjectRoot :: FilePath -> IO (Maybe FilePath)+findProjectRoot dir = do+ normalisedPath <- canonicalizePath dir+ let checkCabalProject d = do+ ex <- doesFileExist fn+ return $ if ex then Just d else Nothing+ where+ fn = d </> "cabal.project"++ checkCabal d = do+ files <- listDirectory' d+ return $ if any (isExtensionOf' ".cabal") files+ then Just d+ else Nothing++ result <- walkUpFolders checkCabalProject normalisedPath+ case result of+ Just rootDir -> pure $ Just rootDir+ Nothing -> walkUpFolders checkCabal normalisedPath+ where+ isExtensionOf' :: String -> FilePath -> Bool+ isExtensionOf' ext fp = ext == takeExtension fp++ listDirectory' :: FilePath -> IO [FilePath]+ listDirectory' fp = filter isSpecialDir <$> getDirectoryContents fp+ where+ isSpecialDir f = f /= "." && f /= ".."++walkUpFolders :: (FilePath -> IO (Maybe a)) -> FilePath -> IO (Maybe a)+walkUpFolders dtest d0 = do+ home <- getHomeDirectory++ let go d | d == home = pure Nothing+ | isDrive d = pure Nothing+ | otherwise = do+ t <- dtest d+ case t of+ Nothing -> go $ takeDirectory d+ x@Just{} -> pure x++ go d0+
lib/GHCup/Prelude/File.hs view
@@ -13,11 +13,7 @@ copyFileE, findFilesDeep, getDirectoryContentsRecursive,- getDirectoryContentsRecursiveBFS,- getDirectoryContentsRecursiveDFS, getDirectoryContentsRecursiveUnsafe,- getDirectoryContentsRecursiveBFSUnsafe,- getDirectoryContentsRecursiveDFSUnsafe, recordedInstallationFile, module GHCup.Prelude.File.Search, @@ -64,7 +60,8 @@ import GHCup.Types.Optics import Text.Regex.Posix-import Control.Monad.IO.Unlift ( MonadUnliftIO )+import Conduit+import qualified Data.Conduit.Combinators as C import Control.Exception.Safe import Control.Monad.Reader import Data.ByteString ( ByteString )@@ -73,7 +70,6 @@ import Text.PrettyPrint.HughesPJClass (prettyShow) import qualified Data.Text as T-import qualified Streamly.Prelude as S import Control.DeepSeq (force) import Control.Exception (evaluate) import GHC.IO.Exception@@ -88,11 +84,12 @@ -- If any copy operation fails, the record file is deleted, as well -- as the partially installed files. mergeFileTree :: ( MonadMask m- , S.MonadAsync m , MonadReader env m , HasDirs env , HasLog env , MonadCatch m+ , MonadIO m+ , MonadUnliftIO m ) => GHCupPath -- ^ source base directory from which to install findFiles -> InstallDirResolved -- ^ destination base dir@@ -127,11 +124,10 @@ -- we want the cleanup action to leak through in case of exception onE_ (cleanupOnPartialInstall recFile) $ wrapInExcepts $ do logDebug "Starting merge"- lift $ flip S.mapM_ (getDirectoryContentsRecursive sourceBase) $ \f -> do- copy f+ lift $ runConduitRes $ getDirectoryContentsRecursive sourceBase .| C.mapM_ (\f -> do+ lift $ copy f logDebug $ T.pack "Recording installed file: " <> T.pack f- recordInstalledFile f recFile- pure f+ recordInstalledFile f recFile) where wrapInExcepts = handleIO (\e -> throwE $ MergeFileTreeError e (fromGHCupPath sourceBase) (fromInstallDir destBase))@@ -182,38 +178,20 @@ copyFileE from to = handleIO (throwE . CopyError . show) . liftIO . copyFile from to --- | List all the files in a directory and all subdirectories.------ The order places files in sub-directories after all the files in their--- parent directories. The list is generated lazily so is not well defined if--- the source directory structure changes before the list is used.------ depth first-getDirectoryContentsRecursiveDFS :: (MonadCatch m, S.MonadAsync m, MonadMask m)- => GHCupPath- -> S.SerialT m FilePath-getDirectoryContentsRecursiveDFS (fromGHCupPath -> fp) = getDirectoryContentsRecursiveDFSUnsafe fp---- breadth first-getDirectoryContentsRecursiveBFS :: (MonadCatch m, S.MonadAsync m, MonadMask m)- => GHCupPath- -> S.SerialT m FilePath-getDirectoryContentsRecursiveBFS (fromGHCupPath -> fp) = getDirectoryContentsRecursiveBFSUnsafe fp---getDirectoryContentsRecursive :: (MonadCatch m, S.MonadAsync m, MonadMask m)+getDirectoryContentsRecursive :: (MonadResource m) => GHCupPath- -> S.SerialT m FilePath-getDirectoryContentsRecursive = getDirectoryContentsRecursiveBFS+ -> ConduitT i FilePath m ()+getDirectoryContentsRecursive (fromGHCupPath -> fp) = getDirectoryContentsRecursiveUnsafe fp -getDirectoryContentsRecursiveUnsafe :: (MonadCatch m, S.MonadAsync m, MonadMask m)+getDirectoryContentsRecursiveUnsafe :: (MonadResource m) => FilePath- -> S.SerialT m FilePath-getDirectoryContentsRecursiveUnsafe = getDirectoryContentsRecursiveBFSUnsafe+ -> ConduitT i FilePath m ()+getDirectoryContentsRecursiveUnsafe = sourceDirectoryDeep' + findFilesDeep :: GHCupPath -> Regex -> IO [FilePath] findFilesDeep path regex =- S.toList $ S.filter (match regex) $ getDirectoryContentsRecursive path+ runResourceT $ sourceToList $ getDirectoryContentsRecursive path .| C.filter (match regex) recordedInstallationFile :: ( MonadReader env m
lib/GHCup/Prelude/File/Posix.hs view
@@ -15,10 +15,8 @@ -} module GHCup.Prelude.File.Posix where -import GHCup.Prelude.File.Posix.Traversals-+import Conduit import Control.Exception.Safe-import Control.Monad.Reader import Foreign.C.String import Foreign.C.Error import Foreign.C.Types@@ -26,7 +24,6 @@ import System.IO.Error hiding ( catchIOError ) import System.FilePath import System.Directory ( removeFile, pathIsSymbolicLink, getSymbolicLinkTarget, doesPathExist )-import System.Posix.Directory import System.Posix.Error ( throwErrnoPathIfMinus1Retry ) import System.Posix.Internals ( withFilePath ) import System.Posix.Files@@ -37,20 +34,13 @@ import qualified System.Posix.Files as PF import qualified System.Posix.IO as SPI import qualified System.Posix as Posix-import qualified Streamly.FileSystem.Handle as FH-import qualified Streamly.Internal.FileSystem.Handle- as IFH-import qualified Streamly.Prelude as S import qualified GHCup.Prelude.File.Posix.Foreign as FD-import qualified Streamly.Internal.Data.Stream.StreamD.Type- as D-import Streamly.Internal.Data.Unfold.Type-import qualified Streamly.Internal.Data.Unfold as U-import Streamly.Internal.Control.Concurrent ( withRunInIO )-import Streamly.Internal.Data.IOFinalizer ( newIOFinalizer, runIOFinalizer )+import GHCup.Prelude.File.Posix.Traversals import GHC.IO.Exception (IOException(ioe_type), IOErrorType (..)) +import qualified Data.Conduit.Combinators as C + -- | On unix, we can use symlinks, so we just get the -- symbolic link target. --@@ -133,14 +123,12 @@ $ \(_, tH) -> do hSetBinaryMode fH True hSetBinaryMode tH True- streamlyCopy (fH, tH)+ runConduitRes $ sourceHandle fH .| sinkHandle tH where openFdHandle fp omode flags fM = do fd <- openFd' fp omode flags fM handle' <- SPI.fdToHandle fd pure (fd, handle')- streamlyCopy (fH, tH) =- S.fold (FH.writeChunks tH) $ IFH.toChunksWithBufferOf (256 * 1024) fH foreign import capi unsafe "fcntl.h open" c_open :: CString -> CInt -> Posix.CMode -> IO CInt@@ -276,60 +264,33 @@ removeEmptyDirectory :: FilePath -> IO () removeEmptyDirectory = PD.removeDirectory ---- | Create an 'Unfold' of directory contents.-unfoldDirContents :: (MonadMask m, MonadIO m, S.MonadAsync m) => Unfold m FilePath (FD.DirType, FilePath)-unfoldDirContents = U.bracket (liftIO . openDirStreamPortable) (liftIO . closeDirStreamPortable) (Unfold step return)- where- {-# INLINE [0] step #-}- step dirstream = do- (typ, e) <- liftIO $ readDirEntPortable dirstream- return $ if- | null e -> D.Stop- | "." == e -> D.Skip dirstream- | ".." == e -> D.Skip dirstream- | otherwise -> D.Yield (typ, e) dirstream---getDirectoryContentsRecursiveDFSUnsafe :: (MonadMask m, MonadIO m, S.MonadAsync m)- => FilePath- -> S.SerialT m FilePath-getDirectoryContentsRecursiveDFSUnsafe fp = go ""- where- go cd = flip S.concatMap (S.unfold unfoldDirContents (fp </> cd)) $ \(t, f) ->- if | t == FD.dtDir -> go (cd </> f)- | otherwise -> pure (cd </> f)---getDirectoryContentsRecursiveUnfold :: (MonadMask m, MonadIO m, S.MonadAsync m) => Unfold m FilePath FilePath-getDirectoryContentsRecursiveUnfold = Unfold step (\s -> return (s, Nothing, [""]))- where- {-# INLINE [0] step #-}- step (_, Nothing, []) = return D.Stop-- step (topdir, Just (cdir, dirstream, finalizer), dirs) = flip onException (runIOFinalizer finalizer) $ do- (dt, f) <- liftIO $ readDirEntPortable dirstream- if | f == "" -> do- runIOFinalizer finalizer- return $ D.Skip (topdir, Nothing, dirs)- | f == "." || f == ".."- -> return $ D.Skip (topdir, Just (cdir, dirstream, finalizer), dirs)- | FD.dtDir == dt -> return $ D.Skip (topdir, Just (cdir, dirstream, finalizer), (cdir </> f):dirs)- | otherwise -> return $ D.Yield (cdir </> f) (topdir, Just (cdir, dirstream, finalizer), dirs)-- step (topdir, Nothing, dir:dirs) = do- (s, f) <- acquire (topdir </> dir)- return $ D.Skip (topdir, Just (dir, s, f), dirs)-- acquire dir =- withRunInIO $ \run -> mask_ $ run $ do- dirstream <- liftIO $ openDirStreamPortable dir- ref <- newIOFinalizer (liftIO $ closeDirStreamPortable dirstream)- return (dirstream, ref)--getDirectoryContentsRecursiveBFSUnsafe :: (MonadMask m, MonadIO m, S.MonadAsync m)- => FilePath- -> S.SerialT m FilePath-getDirectoryContentsRecursiveBFSUnsafe = S.unfold getDirectoryContentsRecursiveUnfold+sourceDirectory' :: MonadResource m => FilePath -> ConduitT i (FD.DirType, FilePath) m ()+sourceDirectory' dir =+ bracketP (openDirStreamPortable dir) closeDirStreamPortable go+ where+ go ds =+ loop+ where+ loop = do+ (typ, e) <- liftIO $ readDirEntPortable ds+ if+ | null e -> return ()+ | "." == e -> loop+ | ".." == e -> loop+ | otherwise -> do+ yield (typ, e)+ loop +sourceDirectoryDeep' :: MonadResource m+ => FilePath -- ^ Root directory+ -> ConduitT i FilePath m ()+sourceDirectoryDeep' fp' = start "" .| C.map snd+ where+ start :: MonadResource m => FilePath -> ConduitT i (FD.DirType, FilePath) m ()+ start dir = sourceDirectory' (fp' </> dir) .| awaitForever go+ where+ go :: MonadResource m => (FD.DirType, FilePath) -> ConduitT (FD.DirType, FilePath) (FD.DirType, FilePath) m ()+ go (typ, fp)+ | FD.dtDir == typ = start (dir </> fp)+ | otherwise = yield (typ, dir </> fp)
lib/GHCup/Prelude/File/Search.hs view
@@ -1,6 +1,7 @@ {-# LANGUAGE CPP #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE DataKinds #-} module GHCup.Prelude.File.Search ( module GHCup.Prelude.File.Search@@ -8,6 +9,7 @@ , CapturedProcess(..) ) where +import GHCup.Prelude.Internal ((!?)) import GHCup.Types(ProcessError(..), CapturedProcess(..)) import Control.Monad.Reader@@ -19,6 +21,7 @@ , removeDirectoryRecursive , removePathForcibly , findFiles+ , makeAbsolute ) import System.FilePath import Text.Regex.Posix@@ -28,7 +31,14 @@ import qualified Text.Megaparsec as MP import Control.Exception.Safe (handleIO) import System.Directory.Internal.Prelude (ioeGetErrorType)+import Haskus.Utils.Variant.Excepts (Excepts)+import GHCup.Errors (NotFoundInPATH(..)) ++makeAbsolute :: MonadIO m => FilePath -> Excepts '[NotFoundInPATH] m FilePath+makeAbsolute bin = do+ spaths <- liftIO getSearchPath+ liftIO (searchPath spaths bin) !? NotFoundInPATH bin -- | Search for a file in the search paths.
lib/GHCup/Prelude/File/Windows.hs view
@@ -16,8 +16,6 @@ import GHCup.Utils.Dirs import GHCup.Prelude.Internal -import Control.Exception.Safe-import Control.Monad import Control.Monad.Reader import Data.List import qualified GHC.Unicode as U@@ -27,15 +25,10 @@ import qualified System.Win32.Info as WS import qualified System.Win32.File as WS -import qualified Streamly.Internal.Data.Stream.StreamD.Type- as D-import Streamly.Internal.Data.Unfold.Type hiding ( concatMap ) import Data.Bits ((.&.))-import qualified Streamly.Prelude as S-import qualified Streamly.Internal.Data.Unfold as U-import Streamly.Internal.Control.Concurrent ( withRunInIO )-import Streamly.Internal.Data.IOFinalizer ( newIOFinalizer, runIOFinalizer ) +import Conduit+import qualified Data.Conduit.Combinators as C -- | On unix, we can use symlinks, so we just get the@@ -101,86 +94,7 @@ removeEmptyDirectory = WS.removeDirectory -unfoldDirContents :: (S.MonadAsync m, MonadIO m, MonadCatch m, MonadMask m) => Unfold m FilePath (WS.FileAttributeOrFlag, FilePath)-unfoldDirContents = U.bracket alloc dealloc (Unfold step return)- where- {-# INLINE [0] step #-}- step (_, False, _, _) = return D.Stop- step (topdir, True, h, fd) = flip onException (liftIO $ WS.findClose h) $ do- f <- liftIO $ WS.getFindDataFileName fd- more <- liftIO $ WS.findNextFile h fd - -- can't get file attribute from FindData yet (needs Win32 PR)- fattr <- liftIO $ WS.getFileAttributes (topdir </> f)-- if | f == "." || f == ".." -> return $ D.Skip (topdir, more, h, fd)- | otherwise -> return $ D.Yield (fattr, f) (topdir, more, h, fd)-- alloc topdir = do- query <- liftIO $ furnishPath (topdir </> "*")- (h, fd) <- liftIO $ WS.findFirstFile query- pure (topdir, True, h, fd)-- dealloc (_, _, fd, _) = liftIO $ WS.findClose fd---getDirectoryContentsRecursiveDFSUnsafe :: (MonadCatch m, S.MonadAsync m, MonadMask m, S.IsStream t)- => FilePath- -> t m FilePath-getDirectoryContentsRecursiveDFSUnsafe fp = go ""- where- isDir attrs = attrs .&. WS.fILE_ATTRIBUTE_DIRECTORY /= 0-- go cd = flip S.concatMap (S.unfold unfoldDirContents (fp </> cd)) $ \(t, f) ->- if | isDir t -> go (cd </> f)- | otherwise -> pure (cd </> f)---getDirectoryContentsRecursiveUnfold :: (MonadCatch m, S.MonadAsync m, MonadMask m) => Unfold m FilePath FilePath-getDirectoryContentsRecursiveUnfold = Unfold step init'- where- {-# INLINE [0] step #-}- step (_, Nothing, []) = return D.Stop-- step (topdir, state@(Just (cdir, (h, findData, ref))), dirs) = flip onException (runIOFinalizer ref) $ do- f <- liftIO $ WS.getFindDataFileName findData-- more <- liftIO $ WS.findNextFile h findData- when (not more) $ runIOFinalizer ref- let nextState = if more then state else Nothing-- -- can't get file attribute from FindData yet (needs Win32 PR)- fattr <- liftIO $ WS.getFileAttributes (topdir </> cdir </> f)-- if | f == "." || f == ".." -> return $ D.Skip (topdir, nextState, dirs)- | isDir fattr -> return $ D.Skip (topdir, nextState, (cdir </> f):dirs)- | otherwise -> return $ D.Yield (cdir </> f) (topdir, nextState, dirs)-- step (topdir, Nothing, dir:dirs) = do- (h, findData, ref) <- acquire (topdir </> dir)- return $ D.Skip (topdir, Just (dir, (h, findData, ref)), dirs)-- init' topdir = do- (h, findData, ref) <- acquire topdir- return (topdir, Just ("", (h, findData, ref)), [])-- isDir attrs = attrs .&. WS.fILE_ATTRIBUTE_DIRECTORY /= 0-- acquire dir = do- query <- liftIO $ furnishPath (dir </> "*")- withRunInIO $ \run -> mask_ $ run $ do- (h, findData) <- liftIO $ WS.findFirstFile query- ref <- newIOFinalizer (liftIO $ WS.findClose h)- return (h, findData, ref)---getDirectoryContentsRecursiveBFSUnsafe :: (MonadMask m, MonadIO m, S.MonadAsync m)- => FilePath- -> S.SerialT m FilePath-getDirectoryContentsRecursiveBFSUnsafe = S.unfold getDirectoryContentsRecursiveUnfold--- -------------------------------------- --[ Inlined from directory package ]-- --------------------------------------@@ -309,3 +223,49 @@ not ('/' `elem` path || "." `elem` splitDirectories path || ".." `elem` splitDirectories path)+++sourceDirectory' :: MonadResource m => FilePath -> ConduitT i (WS.FileAttributeOrFlag, FilePath) m ()+sourceDirectory' topdir =+ bracketP alloc dealloc go+ where+ go (b, h, fd) =+ loop b+ where+ loop False = return ()+ loop True = do+ f <- liftIO $ WS.getFindDataFileName fd+ more <- liftIO $ WS.findNextFile h fd++ -- can't get file attribute from FindData yet (needs Win32 PR)+ fattr <- liftIO $ WS.getFileAttributes (topdir </> f)++ if | f == "." || f == ".." -> loop more+ | otherwise -> do+ yield (fattr, f)+ loop more++ alloc = do+ query <- liftIO $ furnishPath (topdir </> "*")+ (h, fd) <- liftIO $ WS.findFirstFile query+ pure (True, h, fd)++ dealloc (_, fd, _) = liftIO $ WS.findClose fd++sourceDirectoryDeep' :: MonadResource m+ => FilePath -- ^ Root directory+ -> ConduitT i FilePath m ()+sourceDirectoryDeep' fp' = start "" .| C.map snd+ where+ isDir attrs = attrs .&. WS.fILE_ATTRIBUTE_DIRECTORY /= 0++ start :: MonadResource m => FilePath -> ConduitT i (WS.FileAttributeOrFlag, FilePath) m ()+ start dir = sourceDirectory' (fp' </> dir) .| awaitForever go+ where+ go :: MonadResource m+ => (WS.FileAttributeOrFlag, FilePath)+ -> ConduitT (WS.FileAttributeOrFlag, FilePath) (WS.FileAttributeOrFlag, FilePath) m ()+ go (typ, fp)+ | isDir typ = start (dir </> fp)+ | otherwise = yield (typ, dir </> fp)+
lib/GHCup/Prelude/Process/Windows.hs view
@@ -150,7 +150,7 @@ -> m CapturedProcess executeOut' path args chdir env' = do cp <- createProcessWithMingwPath ((proc path args){ cwd = chdir, env = env' })- (exit, out, err) <- liftIO $ readCreateProcessWithExitCodeBS cp ""+ (exit, out, err) <- liftIO $ withRestorePath (env cp) $ readCreateProcessWithExitCodeBS cp "" pure $ CapturedProcess exit out err @@ -166,20 +166,21 @@ -> FilePath -- ^ log filename (opened in append mode) -> Maybe [(String, String)] -- ^ optional environment -> m (Either ProcessError ())-execLogged exe args chdir lfile env = do+execLogged exe args chdir lfile env' = do Dirs {..} <- getDirs logDebug $ T.pack $ "Running " <> exe <> " with arguments " <> show args let stdoutLogfile = fromGHCupPath logsDir </> lfile <> ".stdout.log" stderrLogfile = fromGHCupPath logsDir </> lfile <> ".stderr.log" cp <- createProcessWithMingwPath ((proc exe args) { cwd = chdir- , env = env+ , env = env' , std_in = CreatePipe , std_out = CreatePipe , std_err = CreatePipe }) fmap (toProcessError exe args) $ liftIO+ $ withRestorePath (env cp) $ withCreateProcess cp $ \_ mout merr ph -> case (mout, merr) of@@ -213,16 +214,9 @@ -> Maybe FilePath -- ^ optionally chdir into this -> Maybe [(String, String)] -- ^ optional environment -> m (Either ProcessError ())-exec exe args chdir env = do- -- https://gitlab.haskell.org/haskell/ghcup-hs/-/issues/375- forM_ (Map.fromList <$> env) $ \cEnv -> do- let paths = ["PATH", "Path"]- curPaths = (\x -> maybe [] splitSearchPath (Map.lookup x cEnv)) =<< paths- newPath = intercalate [searchPathSeparator] curPaths- liftIO $ setEnv "PATH" ""- liftIO $ setEnv "Path" newPath- cp <- createProcessWithMingwPath ((proc exe args) { cwd = chdir, env = env })- exit_code <- liftIO $ withCreateProcess cp $ \_ _ _ p -> waitForProcess p+exec exe args chdir env' = do+ cp <- createProcessWithMingwPath ((proc exe args) { cwd = chdir, env = env' })+ exit_code <- liftIO $ withRestorePath (env cp) $ withCreateProcess cp $ \_ _ _ p -> waitForProcess p pure $ toProcessError exe args exit_code -- | Like 'exec', except doesn't add msys2 stuff to PATH.@@ -233,13 +227,6 @@ -> Maybe [(String, String)] -- ^ optional environment -> m (Either ProcessError ()) execNoMinGW exe args chdir env = do- -- https://gitlab.haskell.org/haskell/ghcup-hs/-/issues/375- forM_ (Map.fromList <$> env) $ \cEnv -> do- let paths = ["PATH", "Path"]- curPaths = (\x -> maybe [] splitSearchPath (Map.lookup x cEnv)) =<< paths- newPath = intercalate [searchPathSeparator] curPaths- liftIO $ setEnv "PATH" ""- liftIO $ setEnv "Path" newPath let cp = (proc exe args) { cwd = chdir, env = env } exit_code <- liftIO $ withCreateProcess cp $ \_ _ _ p -> waitForProcess p pure $ toProcessError exe args exit_code@@ -270,7 +257,26 @@ newPath = intercalate [searchPathSeparator] (mingWPaths ++ curPaths) envWithoutPath = foldr (\x y -> Map.delete x y) cEnv paths envWithNewPath = Map.insert "Path" newPath envWithoutPath- liftIO $ setEnv "Path" newPath pure $ cp { env = Just $ Map.toList envWithNewPath } ++withRestorePath :: MonadIO m => Maybe [(String, String)] -- ^ optional env we want to extract 'PATH' from+ -> m a -- ^ action to perform+ -> m a+withRestorePath env action = do+ -- https://gitlab.haskell.org/haskell/ghcup-hs/-/issues/375+ oldPATH <- liftIO $ lookupEnv "PATH"+ oldPath <- liftIO $ lookupEnv "Path"++ forM_ (Map.fromList <$> env) $ \cEnv -> do+ let paths = ["PATH", "Path"]+ curPaths = (\x -> maybe [] splitSearchPath (Map.lookup x cEnv)) =<< paths+ newPath = intercalate [searchPathSeparator] curPaths+ liftIO $ setEnv "PATH" ""+ liftIO $ setEnv "Path" newPath++ r <- action+ liftIO $ maybe (unsetEnv "PATH") (setEnv "PATH") oldPATH+ liftIO $ maybe (unsetEnv "Path") (setEnv "Path") oldPath+ pure r
lib/GHCup/Types.hs view
@@ -207,7 +207,7 @@ | A_Sparc64 | A_ARM | A_ARM64- deriving (Eq, GHC.Generic, Ord, Show)+ deriving (Eq, GHC.Generic, Ord, Show, Bounded, Enum) instance NFData Architecture @@ -379,24 +379,25 @@ instance NFData MetaMode data UserSettings = UserSettings- { uCache :: Maybe Bool- , uMetaCache :: Maybe Integer- , uMetaMode :: Maybe MetaMode- , uNoVerify :: Maybe Bool- , uVerbose :: Maybe Bool- , uKeepDirs :: Maybe KeepDirs- , uDownloader :: Maybe Downloader- , uKeyBindings :: Maybe UserKeyBindings- , uUrlSource :: Maybe URLSource- , uNoNetwork :: Maybe Bool- , uGPGSetting :: Maybe GPGSetting- , uPlatformOverride :: Maybe PlatformRequest- , uMirrors :: Maybe DownloadMirrors+ { uCache :: Maybe Bool+ , uMetaCache :: Maybe Integer+ , uMetaMode :: Maybe MetaMode+ , uNoVerify :: Maybe Bool+ , uVerbose :: Maybe Bool+ , uKeepDirs :: Maybe KeepDirs+ , uDownloader :: Maybe Downloader+ , uKeyBindings :: Maybe UserKeyBindings+ , uUrlSource :: Maybe URLSource+ , uNoNetwork :: Maybe Bool+ , uGPGSetting :: Maybe GPGSetting+ , uPlatformOverride :: Maybe PlatformRequest+ , uMirrors :: Maybe DownloadMirrors+ , uDefGHCConfOptions :: Maybe [String] } deriving (Show, GHC.Generic, Eq) defaultUserSettings :: UserSettings-defaultUserSettings = UserSettings Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing+defaultUserSettings = UserSettings Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing fromSettings :: Settings -> Maybe KeyBindings -> UserSettings fromSettings Settings{..} Nothing =@@ -414,6 +415,7 @@ , uGPGSetting = Just gpgSetting , uPlatformOverride = platformOverride , uMirrors = Just mirrors+ , uDefGHCConfOptions = Just defGHCConfOptions } fromSettings Settings{..} (Just KeyBindings{..}) = let ukb = UserKeyBindings@@ -440,6 +442,7 @@ , uGPGSetting = Just gpgSetting , uPlatformOverride = platformOverride , uMirrors = Just mirrors+ , uDefGHCConfOptions = Just defGHCConfOptions } data UserKeyBindings = UserKeyBindings@@ -512,19 +515,20 @@ data Settings = Settings- { cache :: Bool- , metaCache :: Integer- , metaMode :: MetaMode- , noVerify :: Bool- , keepDirs :: KeepDirs- , downloader :: Downloader- , verbose :: Bool- , urlSource :: URLSource- , noNetwork :: Bool- , gpgSetting :: GPGSetting- , noColor :: Bool -- this also exists in LoggerConfig- , platformOverride :: Maybe PlatformRequest- , mirrors :: DownloadMirrors+ { cache :: Bool+ , metaCache :: Integer+ , metaMode :: MetaMode+ , noVerify :: Bool+ , keepDirs :: KeepDirs+ , downloader :: Downloader+ , verbose :: Bool+ , urlSource :: URLSource+ , noNetwork :: Bool+ , gpgSetting :: GPGSetting+ , noColor :: Bool -- this also exists in LoggerConfig+ , platformOverride :: Maybe PlatformRequest+ , mirrors :: DownloadMirrors+ , defGHCConfOptions :: [String] } deriving (Show, GHC.Generic) @@ -532,7 +536,7 @@ defaultMetaCache = 300 -- 5 minutes defaultSettings :: Settings-defaultSettings = Settings False defaultMetaCache Lax False Never Curl False GHCupURL False GPGNone False Nothing (DM mempty)+defaultSettings = Settings False defaultMetaCache Lax False Never Curl False GHCupURL False GPGNone False Nothing (DM mempty) [] instance NFData Settings
lib/GHCup/Utils.hs view
@@ -59,6 +59,7 @@ #if !MIN_VERSION_base(4,13,0) import Control.Monad.Fail ( MonadFail ) #endif+import Conduit ((.|), runConduitRes) import Control.Monad.Reader import Control.Monad.Trans.Resource hiding ( throwM )@@ -87,7 +88,7 @@ import qualified Data.Text.Encoding as E import qualified Text.Megaparsec as MP import qualified Data.List.NonEmpty as NE-import qualified Streamly.Prelude as S+import qualified Data.Conduit.Combinators as C import Control.DeepSeq (force) import GHC.IO (evaluate)@@ -1169,12 +1170,13 @@ installDestSanityCheck :: ( MonadIO m , MonadCatch m , MonadMask m+ , MonadUnliftIO m ) => InstallDirResolved -> Excepts '[DirNotEmpty] m () installDestSanityCheck (IsolateDirResolved isoDir) = do hideErrorDef [doesNotExistErrorType] () $ do- empty' <- liftIO $ S.null $ getDirectoryContentsRecursiveUnsafe isoDir+ empty' <- lift $ runConduitRes $ getDirectoryContentsRecursiveUnsafe isoDir .| C.null when (not empty') (throwE $ DirNotEmpty isoDir) installDestSanityCheck _ = pure ()
lib/GHCup/Utils/Dirs.hs view
@@ -143,6 +143,7 @@ , removeDirectoryRecursive , removePathForcibly , findFiles+ , makeAbsolute ) import qualified System.Directory as SD
+ lib/GHCup/Utils/Parsers.hs view
@@ -0,0 +1,392 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE DuplicateRecordFields #-}+{-# LANGUAGE ViewPatterns #-}++module GHCup.Utils.Parsers where+++import GHCup.Errors+import GHCup.Types+import GHCup.Types.Optics+import GHCup.List+import GHCup.Utils+import GHCup.Prelude+import GHCup.Prelude.Logger+import GHCup.Prelude.MegaParsec++import Control.Applicative ((<|>))+import Control.Exception.Safe+#if !MIN_VERSION_base(4,13,0)+import Control.Monad.Fail ( MonadFail )+#endif+import Control.Monad.Reader+import Data.Aeson+#if MIN_VERSION_aeson(2,0,0)+#else+import qualified Data.HashMap.Strict as KM+#endif+import Data.Bifunctor+import Data.Char+import Data.Either+import Data.Functor+import Data.List ( sort, sortBy )+import Data.Maybe+import Data.Text ( Text )+import Data.Time.Calendar ( Day )+import Data.Time.Format ( parseTimeM, defaultTimeLocale )+import Data.Versions+import Data.Void+import Haskus.Utils.Variant.Excepts+import Prelude hiding ( appendFile )+import Safe+import System.FilePath+import URI.ByteString hiding (parseURI)++import qualified Data.ByteString.UTF8 as UTF8+import qualified Data.Text as T+import qualified Data.Text.Lazy.Encoding as LE+import qualified Data.Text.Lazy as LT+import qualified Text.Megaparsec as MP+import GHCup.Version+++ -------------+ --[ Types ]--+ -------------++-- a superset of ToolVersion+data SetToolVersion = SetGHCVersion GHCTargetVersion+ | SetToolVersion Version+ | SetToolTag Tag+ | SetToolDay Day+ | SetRecommended+ | SetNext+ deriving (Eq, Show)++prettyToolVer :: ToolVersion -> String+prettyToolVer (GHCVersion v') = T.unpack $ tVerToText v'+prettyToolVer (ToolVersion v') = T.unpack $ prettyVer v'+prettyToolVer (ToolTag t) = show t+prettyToolVer (ToolDay day) = show day++toSetToolVer :: Maybe ToolVersion -> SetToolVersion+toSetToolVer (Just (GHCVersion v')) = SetGHCVersion v'+toSetToolVer (Just (ToolVersion v')) = SetToolVersion v'+toSetToolVer (Just (ToolTag t')) = SetToolTag t'+toSetToolVer (Just (ToolDay d')) = SetToolDay d'+toSetToolVer Nothing = SetRecommended+++platformParser :: String -> Either String PlatformRequest+platformParser s' = case MP.parse (platformP <* MP.eof) "" (T.pack s') of+ Right r -> pure r+ Left e -> Left $ errorBundlePretty e+ where+ archP :: MP.Parsec Void Text Architecture+ archP = choice' ((\x -> MP.chunk (T.pack $ archToString x) $> x) <$> ([minBound..maxBound] :: [Architecture]))+ platformP :: MP.Parsec Void Text PlatformRequest+ platformP = choice'+ [ (`PlatformRequest` FreeBSD)+ <$> (archP <* MP.chunk "-")+ <*> ( MP.chunk "portbld"+ *> ( MP.try (Just <$> verP (MP.chunk "-freebsd" <* MP.eof))+ <|> pure Nothing+ )+ <* MP.chunk "-freebsd"+ )+ , (`PlatformRequest` Darwin)+ <$> (archP <* MP.chunk "-")+ <*> ( MP.chunk "apple"+ *> ( MP.try (Just <$> verP (MP.chunk "-darwin" <* MP.eof))+ <|> pure Nothing+ )+ <* MP.chunk "-darwin"+ )+ , (\a d mv -> PlatformRequest a (Linux d) mv)+ <$> (archP <* MP.chunk "-")+ <*> distroP+ <*> ((MP.try (Just <$> verP (MP.chunk "-linux" <* MP.eof)) <|> pure Nothing+ )+ <* MP.chunk "-linux"+ )+ , (\a -> PlatformRequest a Windows Nothing)+ <$> ((archP <* MP.chunk "-")+ <* (MP.chunk "unknown-mingw32" <|> MP.chunk "unknown-windows" <|> MP.chunk "windows"))+ ]+ distroP :: MP.Parsec Void Text LinuxDistro+ distroP = choice' ((\d -> MP.chunk (T.pack $ distroToString d) $> d) <$> allDistros)+++uriParser :: String -> Either String URI+uriParser = first show . parseURI . UTF8.fromString+++absolutePathParser :: FilePath -> Either String FilePath+absolutePathParser f = case isValid f && isAbsolute f of+ True -> Right $ normalise f+ False -> Left "Please enter a valid absolute filepath."++isolateParser :: FilePath -> Either String FilePath+isolateParser f = case isValid f && isAbsolute f of+ True -> Right $ normalise f+ False -> Left "Please enter a valid filepath for isolate dir."++-- this accepts cross prefix+ghcVersionTagEither :: String -> Either String ToolVersion+ghcVersionTagEither s' =+ second ToolDay (dayParser s') <|> second ToolTag (tagEither s') <|> second GHCVersion (ghcVersionEither s')++-- this ignores cross prefix+toolVersionTagEither :: String -> Either String ToolVersion+toolVersionTagEither s' =+ second ToolDay (dayParser s') <|> second ToolTag (tagEither s') <|> second ToolVersion (toolVersionEither s')++tagEither :: String -> Either String Tag+tagEither s' = case fmap toLower s' of+ "recommended" -> Right Recommended+ "latest" -> Right Latest+ "latest-prerelease" -> Right LatestPrerelease+ "latest-nightly" -> Right LatestNightly+ ('b':'a':'s':'e':'-':ver') -> case pvp (T.pack ver') of+ Right x -> Right (Base x)+ Left _ -> Left $ "Invalid PVP version for base " <> ver'+ other -> Left $ "Unknown tag " <> other+++ghcVersionEither :: String -> Either String GHCTargetVersion+ghcVersionEither =+ first (const "Not a valid version") . MP.parse ghcTargetVerP "" . T.pack++toolVersionEither :: String -> Either String Version+toolVersionEither =+ first (const "Not a valid version") . MP.parse (version' <* MP.eof) "" . T.pack+++toolParser :: String -> Either String Tool+toolParser s' | t == T.pack "ghc" = Right GHC+ | t == T.pack "cabal" = Right Cabal+ | t == T.pack "hls" = Right HLS+ | t == T.pack "stack" = Right Stack+ | otherwise = Left ("Unknown tool: " <> s')+ where t = T.toLower (T.pack s')++dayParser :: String -> Either String Day+dayParser s = maybe (Left $ "Could not parse \"" <> s <> "\". Expected format is: YYYY-MM-DD") Right+ $ parseTimeM True defaultTimeLocale "%Y-%-m-%-d" s+++criteriaParser :: String -> Either String ListCriteria+criteriaParser s' | t == T.pack "installed" = Right $ ListInstalled True+ | t == T.pack "set" = Right $ ListSet True+ | t == T.pack "available" = Right $ ListAvailable True+ | t == T.pack "+installed" = Right $ ListInstalled True+ | t == T.pack "+set" = Right $ ListSet True+ | t == T.pack "+available" = Right $ ListAvailable True+ | t == T.pack "-installed" = Right $ ListInstalled False+ | t == T.pack "-set" = Right $ ListSet False+ | t == T.pack "-available" = Right $ ListAvailable False+ | otherwise = Left ("Unknown criteria: " <> s')+ where t = T.toLower (T.pack s')++++keepOnParser :: String -> Either String KeepDirs+keepOnParser s' | t == T.pack "always" = Right Always+ | t == T.pack "errors" = Right Errors+ | t == T.pack "never" = Right Never+ | otherwise = Left ("Unknown keep value: " <> s')+ where t = T.toLower (T.pack s')+++downloaderParser :: String -> Either String Downloader+downloaderParser s' | t == T.pack "curl" = Right Curl+ | t == T.pack "wget" = Right Wget+#if defined(INTERNAL_DOWNLOADER)+ | t == T.pack "internal" = Right Internal+#endif+ | otherwise = Left ("Unknown downloader value: " <> s')+ where t = T.toLower (T.pack s')++gpgParser :: String -> Either String GPGSetting+gpgParser s' | t == T.pack "strict" = Right GPGStrict+ | t == T.pack "lax" = Right GPGLax+ | t == T.pack "none" = Right GPGNone+ | otherwise = Left ("Unknown gpg setting value: " <> s')+ where t = T.toLower (T.pack s')++++overWriteVersionParser :: String -> Either String [VersionPattern]+overWriteVersionParser = first (const "Not a valid version pattern") . MP.parse (MP.many versionPattern <* MP.eof) "" . T.pack+ where+ versionPattern :: MP.Parsec Void Text VersionPattern+ versionPattern = do+ str' <- T.unpack <$> MP.takeWhileP Nothing (/= '%')+ if str' /= mempty+ then pure (S str')+ else fmap (const CabalVer) v_cabal+ <|> fmap (const GitBranchName) b_name+ <|> fmap (const GitHashShort) s_hash+ <|> fmap (const GitHashLong) l_hash+ <|> fmap (const GitDescribe) g_desc+ <|> ((\a b -> S (a : T.unpack b)) <$> MP.satisfy (const True) <*> MP.takeWhileP Nothing (== '%')) -- invalid pattern, e.g. "%k"+ where+ v_cabal = MP.chunk "%v"+ b_name = MP.chunk "%b"+ s_hash = MP.chunk "%h"+ l_hash = MP.chunk "%H"+ g_desc = MP.chunk "%g"++ -----------------+ --[ Utilities ]--+ -----------------+++fromVersion :: ( HasLog env+ , MonadFail m+ , MonadReader env m+ , HasGHCupInfo env+ , HasDirs env+ , MonadThrow m+ , MonadIO m+ , MonadCatch m+ )+ => Maybe ToolVersion+ -> Tool+ -> Excepts+ '[ TagNotFound+ , DayNotFound+ , NextVerNotFound+ , NoToolVersionSet+ ] m (GHCTargetVersion, Maybe VersionInfo)+fromVersion tv = fromVersion' (toSetToolVer tv)++fromVersion' :: ( HasLog env+ , MonadFail m+ , MonadReader env m+ , HasGHCupInfo env+ , HasDirs env+ , MonadThrow m+ , MonadIO m+ , MonadCatch m+ )+ => SetToolVersion+ -> Tool+ -> Excepts+ '[ TagNotFound+ , DayNotFound+ , NextVerNotFound+ , NoToolVersionSet+ ] m (GHCTargetVersion, Maybe VersionInfo)+fromVersion' SetRecommended tool = do+ GHCupInfo { _ghcupDownloads = dls } <- lift getGHCupInfo+ second Just <$> getRecommended dls tool+ ?? TagNotFound Recommended tool+fromVersion' (SetGHCVersion v) tool = do+ GHCupInfo { _ghcupDownloads = dls } <- lift getGHCupInfo+ let vi = getVersionInfo v tool dls+ case pvp $ prettyVer (_tvVersion v) of -- need to be strict here+ Left _ -> pure (v, vi)+ Right pvpIn ->+ lift (getLatestToolFor tool (_tvTarget v) pvpIn dls) >>= \case+ Just (pvp_, vi', mt) -> do+ v' <- lift $ pvpToVersion pvp_ ""+ when (v' /= _tvVersion v) $ lift $ logWarn ("Assuming you meant version " <> prettyVer v')+ pure (GHCTargetVersion mt v', Just vi')+ Nothing -> pure (v, vi)+fromVersion' (SetToolVersion (mkTVer -> v)) tool = do+ GHCupInfo { _ghcupDownloads = dls } <- lift getGHCupInfo+ let vi = getVersionInfo v tool dls+ case pvp $ prettyVer (_tvVersion v) of -- need to be strict here+ Left _ -> pure (v, vi)+ Right pvpIn ->+ lift (getLatestToolFor tool (_tvTarget v) pvpIn dls) >>= \case+ Just (pvp_, vi', mt) -> do+ v' <- lift $ pvpToVersion pvp_ ""+ when (v' /= _tvVersion v) $ lift $ logWarn ("Assuming you meant version " <> prettyVer v')+ pure (GHCTargetVersion mt v', Just vi')+ Nothing -> pure (v, vi)+fromVersion' (SetToolTag Latest) tool = do+ GHCupInfo { _ghcupDownloads = dls } <- lift getGHCupInfo+ second Just <$> getLatest dls tool ?? TagNotFound Latest tool+fromVersion' (SetToolDay day) tool = do+ GHCupInfo { _ghcupDownloads = dls } <- lift getGHCupInfo+ second Just <$> case getByReleaseDay dls tool day of+ Left ad -> throwE $ DayNotFound day tool ad+ Right v -> pure v+fromVersion' (SetToolTag LatestPrerelease) tool = do+ GHCupInfo { _ghcupDownloads = dls } <- lift getGHCupInfo+ second Just <$> getLatestPrerelease dls tool ?? TagNotFound LatestPrerelease tool+fromVersion' (SetToolTag LatestNightly) tool = do+ GHCupInfo { _ghcupDownloads = dls } <- lift getGHCupInfo+ second Just <$> getLatestNightly dls tool ?? TagNotFound LatestNightly tool+fromVersion' (SetToolTag Recommended) tool = do+ GHCupInfo { _ghcupDownloads = dls } <- lift getGHCupInfo+ second Just <$> getRecommended dls tool ?? TagNotFound Recommended tool+fromVersion' (SetToolTag (Base pvp'')) GHC = do+ GHCupInfo { _ghcupDownloads = dls } <- lift getGHCupInfo+ second Just <$> getLatestBaseVersion dls pvp'' ?? TagNotFound (Base pvp'') GHC+fromVersion' SetNext tool = do+ GHCupInfo { _ghcupDownloads = dls } <- lift getGHCupInfo+ next <- case tool of+ GHC -> do+ set <- fmap _tvVersion $ ghcSet Nothing !? NoToolVersionSet tool+ ghcs <- rights <$> lift getInstalledGHCs+ (headMay+ . tail+ . dropWhile (\GHCTargetVersion {..} -> _tvVersion /= set)+ . cycle+ . sortBy (\x y -> compare (_tvVersion x) (_tvVersion y))+ . filter (\GHCTargetVersion {..} -> isNothing _tvTarget)+ $ ghcs) ?? NoToolVersionSet tool+ Cabal -> do+ set <- cabalSet !? NoToolVersionSet tool+ cabals <- rights <$> lift getInstalledCabals+ (fmap (GHCTargetVersion Nothing)+ . headMay+ . tail+ . dropWhile (/= set)+ . cycle+ . sort+ $ cabals) ?? NoToolVersionSet tool+ HLS -> do+ set <- hlsSet !? NoToolVersionSet tool+ hlses <- rights <$> lift getInstalledHLSs+ (fmap (GHCTargetVersion Nothing)+ . headMay+ . tail+ . dropWhile (/= set)+ . cycle+ . sort+ $ hlses) ?? NoToolVersionSet tool+ Stack -> do+ set <- stackSet !? NoToolVersionSet tool+ stacks <- rights <$> lift getInstalledStacks+ (fmap (GHCTargetVersion Nothing)+ . headMay+ . tail+ . dropWhile (/= set)+ . cycle+ . sort+ $ stacks) ?? NoToolVersionSet tool+ GHCup -> fail "GHCup cannot be set"+ let vi = getVersionInfo next tool dls+ pure (next, vi)+fromVersion' (SetToolTag t') tool =+ throwE $ TagNotFound t' tool+++parseUrlSource :: String -> Either String URLSource+parseUrlSource "GHCupURL" = pure GHCupURL+parseUrlSource "StackSetupURL" = pure StackSetupURL+parseUrlSource s' = (eitherDecode . LE.encodeUtf8 . LT.pack $ s')+ <|> (fmap (OwnSource . (:[]) . Right) . first show . parseURI .UTF8.fromString $ s')++parseNewUrlSource :: String -> Either String NewURLSource+parseNewUrlSource "GHCupURL" = pure NewGHCupURL+parseNewUrlSource "StackSetupURL" = pure NewStackSetupURL+parseNewUrlSource s' = (eitherDecode . LE.encodeUtf8 . LT.pack $ s')+ <|> (fmap NewURI . first show . parseURI .UTF8.fromString $ s')
test/ghcup-test/GHCup/Utils/FileSpec.hs view
@@ -2,11 +2,11 @@ import GHCup.Prelude.File +import Conduit import Data.List import System.Directory import System.FilePath import System.IO.Unsafe-import qualified Streamly.Prelude as S import Test.Hspec @@ -15,14 +15,8 @@ spec :: Spec spec = do describe "GHCup.Utils.File" $ do- it "getDirectoryContentsRecursiveBFS" $ do- l1 <- sort <$> S.toList (getDirectoryContentsRecursiveBFSUnsafe "lib")- l2 <- sort <$> getDirectoryContentsRecursiveLazy "lib"- not (null l1) `shouldBe` True- not (null l2) `shouldBe` True- l1 `shouldBe` l2- it "getDirectoryContentsRecursiveDFS" $ do- l1 <- sort <$> S.toList (getDirectoryContentsRecursiveDFSUnsafe "lib")+ it "getDirectoryContentsRecursiveUnsafe" $ do+ l1 <- sort <$> runResourceT (sourceToList $ getDirectoryContentsRecursiveUnsafe "lib") l2 <- sort <$> getDirectoryContentsRecursiveLazy "lib" not (null l1) `shouldBe` True not (null l2) `shouldBe` True
test/optparse-test/CompileTest.hs view
@@ -31,6 +31,7 @@ boot Nothing Nothing+ Nothing (Just $ Right []) Nothing []@@ -78,8 +79,8 @@ ) , (baseCmd <> "-j20", baseOptions{GHC.jobs = Just 20}) , (baseCmd <> "--jobs 10", baseOptions{GHC.jobs = Just 10})- , (baseCmd <> "-c build.mk", baseOptions{GHC.buildConfig = Just "build.mk"})- , (baseCmd <> "--config build.mk", baseOptions{GHC.buildConfig = Just "build.mk"})+ , (baseCmd <> "-c build.mk", baseOptions{GHC.buildConfig = Just "build.mk", GHC.buildSystem = Just Make})+ , (baseCmd <> "--config build.mk", baseOptions{GHC.buildConfig = Just "build.mk", GHC.buildSystem = Just Make}) #ifdef IS_WINDOWS , (baseCmd <> "--patch file:c:/example.patch", baseOptions{GHC.patches = Just $ Right [[uri|file:c:/example.patch|]]}) #else@@ -96,6 +97,7 @@ , (baseCmd <> "-f make", baseOptions{GHC.buildFlavour = Just "make"}) , (baseCmd <> "--flavour make", baseOptions{GHC.buildFlavour = Just "make"}) , (baseCmd <> "--hadrian", baseOptions{GHC.buildSystem = Just Hadrian})+ , (baseCmd <> "--hadrian --hadrian-ghc 9.2.8", baseOptions{GHC.buildSystem = Just Hadrian, GHC.hadrianGhc = Just (Left $(versionQ "9.2.8"))}) , (baseCmd <> "--make", baseOptions{GHC.buildSystem = Just Make}) #ifdef IS_WINDOWS , (baseCmd <> "-i C:\\\\tmp\\out_dir", baseOptions{GHC.isolateDir = Just "C:\\tmp\\out_dir"})
test/optparse-test/SetTest.hs view
@@ -4,6 +4,7 @@ module SetTest where import GHCup.OptParse+import GHCup.Utils.Parsers (SetToolVersion(..)) import Test.Tasty import GHCup.Types import Data.Versions