Skip to content

Commit 54f45b5

Browse files
authored
Merge pull request #5585 from hasufell/jospald/GHC-install-hooks
Support custom GHC installation hooks
2 parents 1ddf6c0 + 48cc5d2 commit 54f45b5

File tree

8 files changed

+181
-17
lines changed

8 files changed

+181
-17
lines changed

ChangeLog.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ Other enhancements:
5959
* `stack upload` supports authentication with a Hackage API key (via
6060
`HACKAGE_KEY` environment variable).
6161

62+
* Add GHC installation hooks wrt [#5585](https://github.com/commercialhaskell/stack/pull/5585)
63+
6264
Bug fixes:
6365

6466
* Ensure that `extra-path` works for case-insensitive `PATH`s on Windows.

doc/yaml_configuration.md

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1199,3 +1199,65 @@ This field is convenient in setups that restrict access to GitHub, for instance
11991199

12001200

12011201
Since 2.5.0
1202+
1203+
## Hooks
1204+
1205+
### GHC installation hooks (experimental)
1206+
1207+
Stack's installation procedure can be fully customized by placing a shell script at
1208+
`~/.stack/hooks/ghc-install.sh` and making it executable.
1209+
1210+
The script **must** return an exit code of `0` and the standard output **must** be the
1211+
absolute path to the ghc binary that was installed. Otherwise stack will ignore
1212+
the hook and possibly fall back to its own installation procedure.
1213+
1214+
Hooks are not run when `system-ghc: true`.
1215+
1216+
When `install-ghc: false`, hooks are still run,
1217+
which allows you to ensure that only your hook will install GHC and stack won't default
1218+
to its own installation logic, even when the hook fails.
1219+
1220+
An example hook is:
1221+
1222+
```sh
1223+
#!/bin/sh
1224+
1225+
set -eu
1226+
1227+
case $HOOK_GHC_TYPE in
1228+
bindist)
1229+
# install GHC here, not printing to stdout, e.g.:
1230+
# command install $HOOK_GHC_VERSION >/dev/null
1231+
;;
1232+
git)
1233+
>&2 echo "Hook doesn't support installing from source"
1234+
exit 1
1235+
;;
1236+
*)
1237+
>&2 echo "Unsupported GHC installation type: $HOOK_GHC_TYPE"
1238+
exit 2
1239+
;;
1240+
esac
1241+
1242+
echo "location/to/ghc/executable"
1243+
```
1244+
1245+
The following environment variables are always passed to the hook:
1246+
1247+
* `HOOK_GHC_TYPE = "bindist" | "git" | "ghcjs"`
1248+
1249+
For "bindist", additional variables are:
1250+
1251+
* `HOOK_GHC_VERSION = <ver>`
1252+
1253+
For "git", additional variables are:
1254+
1255+
* `HOOK_GHC_COMMIT = <commit>`
1256+
* `HOOK_GHC_FLAVOR = <flavor>`
1257+
1258+
For "ghcjs", additional variables are:
1259+
1260+
* `HOOK_GHC_VERSION = <ver>`
1261+
* `HOOK_GHCJS_VERSION = <ver>`
1262+
1263+
Since 2.8.X

src/Stack/Setup.hs

Lines changed: 79 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,9 @@ import Data.List hiding (concat, elem, maximumBy, any)
5252
import qualified Data.Map as Map
5353
import qualified Data.Set as Set
5454
import qualified Data.Text as T
55+
import qualified Data.Text.Lazy as TL
5556
import qualified Data.Text.Encoding as T
57+
import qualified Data.Text.Lazy.Encoding as TL
5658
import qualified Data.Text.Encoding.Error as T
5759
import qualified Data.Yaml as Yaml
5860
import Distribution.System (OS, Arch (..), Platform (..))
@@ -445,21 +447,18 @@ ensureCompilerAndMsys
445447
=> SetupOpts
446448
-> RIO env (CompilerPaths, ExtraDirs)
447449
ensureCompilerAndMsys sopts = do
450+
getSetupInfo' <- memoizeRef getSetupInfo
451+
mmsys2Tool <- ensureMsys sopts getSetupInfo'
452+
msysPaths <- maybe (pure Nothing) (fmap Just . extraDirs) mmsys2Tool
453+
448454
actual <- either throwIO pure $ wantedToActual $ soptsWantedCompiler sopts
449455
didWarn <- warnUnsupportedCompiler $ getGhcVersion actual
450456

451-
getSetupInfo' <- memoizeRef getSetupInfo
452457
(cp, ghcPaths) <- ensureCompiler sopts getSetupInfo'
453458

454459
warnUnsupportedCompilerCabal cp didWarn
455460

456-
mmsys2Tool <- ensureMsys sopts getSetupInfo'
457-
paths <-
458-
case mmsys2Tool of
459-
Nothing -> pure ghcPaths
460-
Just msys2Tool -> do
461-
msys2Paths <- extraDirs msys2Tool
462-
pure $ ghcPaths <> msys2Paths
461+
let paths = maybe ghcPaths (ghcPaths <>) msysPaths
463462
pure (cp, paths)
464463

465464
-- | See <https://github.com/commercialhaskell/stack/issues/4246>
@@ -602,13 +601,18 @@ installGhcBindist sopts getSetupInfo' installed = do
602601

603602
-- | Ensure compiler is installed, without worrying about msys
604603
ensureCompiler
605-
:: forall env. (HasBuildConfig env, HasGHCVariant env)
604+
:: forall env. (HasConfig env, HasBuildConfig env, HasGHCVariant env)
606605
=> SetupOpts
607606
-> Memoized SetupInfo
608607
-> RIO env (CompilerPaths, ExtraDirs)
609608
ensureCompiler sopts getSetupInfo' = do
610609
let wanted = soptsWantedCompiler sopts
611610
wc <- either throwIO (pure . whichCompiler) $ wantedToActual wanted
611+
612+
hook <- ghcInstallHook
613+
hookIsExecutable <- handleIO (\_ -> pure False) $ if osIsWindows
614+
then doesFileExist hook -- can't really detect executable on windows, only file extension
615+
else executable <$> getPermissions hook
612616

613617
Platform expectedArch _ <- view platformL
614618

@@ -629,20 +633,78 @@ ensureCompiler sopts getSetupInfo' = do
629633
Right cp -> pure $ Just cp
630634

631635
mcp <-
632-
if soptsUseSystem sopts
633-
then do
634-
logDebug "Getting system compiler version"
635-
runConduit $
636-
sourceSystemCompilers wanted .|
637-
concatMapMC checkCompiler .|
638-
await
639-
else return Nothing
636+
if | soptsUseSystem sopts -> do
637+
logDebug "Getting system compiler version"
638+
runConduit $
639+
sourceSystemCompilers wanted .|
640+
concatMapMC checkCompiler .|
641+
await
642+
| hookIsExecutable -> do
643+
-- if the hook fails, we fall through to stacks sandboxed installation
644+
hookGHC <- runGHCInstallHook sopts hook
645+
maybe (pure Nothing) checkCompiler hookGHC
646+
| otherwise -> return Nothing
640647
case mcp of
641648
Nothing -> ensureSandboxedCompiler sopts getSetupInfo'
642649
Just cp -> do
643650
let paths = ExtraDirs { edBins = [parent $ cpCompiler cp], edInclude = [], edLib = [] }
644651
pure (cp, paths)
645652

653+
654+
-- | Runs @STACK_ROOT\/hooks\/ghc-install.sh@.
655+
--
656+
-- Reads and possibly validates the output of the process as the GHC
657+
-- binary and returns it.
658+
runGHCInstallHook
659+
:: HasBuildConfig env
660+
=> SetupOpts
661+
-> Path Abs File
662+
-> RIO env (Maybe (Path Abs File))
663+
runGHCInstallHook sopts hook = do
664+
logDebug "Getting hook installed compiler version"
665+
let wanted = soptsWantedCompiler sopts
666+
menv0 <- view processContextL
667+
menv <- mkProcessContext (Map.union (wantedCompilerToEnv wanted) $
668+
removeHaskellEnvVars (view envVarsL menv0))
669+
(exit, out) <- withProcessContext menv $ proc "sh" [toFilePath hook] readProcessStdout
670+
case exit of
671+
ExitSuccess -> do
672+
let ghcPath = stripNewline . TL.unpack . TL.decodeUtf8With T.lenientDecode $ out
673+
case parseAbsFile ghcPath of
674+
Just compiler -> do
675+
when (soptsSanityCheck sopts) $ sanityCheck compiler
676+
logDebug ("Using GHC compiler at: " <> fromString (toFilePath compiler))
677+
pure (Just compiler)
678+
Nothing -> do
679+
logWarn ("Path to GHC binary is not a valid path: " <> fromString ghcPath)
680+
pure Nothing
681+
ExitFailure i -> do
682+
logWarn ("GHC install hook exited with code: " <> fromString (show i))
683+
pure Nothing
684+
where
685+
wantedCompilerToEnv :: WantedCompiler -> EnvVars
686+
wantedCompilerToEnv (WCGhc ver) =
687+
Map.fromList [("HOOK_GHC_TYPE", "bindist")
688+
,("HOOK_GHC_VERSION", T.pack (versionString ver))
689+
]
690+
wantedCompilerToEnv (WCGhcGit commit flavor) =
691+
Map.fromList [("HOOK_GHC_TYPE", "git")
692+
,("HOOK_GHC_COMMIT", commit)
693+
,("HOOK_GHC_FLAVOR", flavor)
694+
,("HOOK_GHC_FLAVOUR", flavor)
695+
]
696+
wantedCompilerToEnv (WCGhcjs ghcjs_ver ghc_ver) =
697+
Map.fromList [("HOOK_GHC_TYPE", "ghcjs")
698+
,("HOOK_GHC_VERSION", T.pack (versionString ghc_ver))
699+
,("HOOK_GHCJS_VERSION", T.pack (versionString ghcjs_ver))
700+
]
701+
newlines :: [Char]
702+
newlines = ['\n', '\r']
703+
704+
stripNewline :: String -> String
705+
stripNewline str = filter (flip notElem newlines) str
706+
707+
646708
ensureSandboxedCompiler
647709
:: HasBuildConfig env
648710
=> SetupOpts

src/Stack/Types/Config.hs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
{-# LANGUAGE MultiParamTypeClasses #-}
1212
{-# LANGUAGE MultiWayIf #-}
1313
{-# LANGUAGE OverloadedStrings #-}
14+
{-# LANGUAGE QuasiQuotes #-}
1415
{-# LANGUAGE RecordWildCards #-}
1516
{-# LANGUAGE ScopedTypeVariables #-}
1617
{-# LANGUAGE TypeFamilies #-}
@@ -128,6 +129,7 @@ module Stack.Types.Config
128129
,shaPath
129130
,shaPathForBytes
130131
,workDirL
132+
,ghcInstallHook
131133
-- * Command-related types
132134
,AddCommand
133135
-- ** Eval
@@ -1320,6 +1322,18 @@ askLatestSnapshotUrl = view $ configL.to configLatestSnapshot
13201322
workDirL :: HasConfig env => Lens' env (Path Rel Dir)
13211323
workDirL = configL.lens configWorkDir (\x y -> x { configWorkDir = y })
13221324

1325+
-- | @STACK_ROOT\/hooks\/@
1326+
hooksDir :: HasConfig env => RIO env (Path Abs Dir)
1327+
hooksDir = do
1328+
sr <- view $ configL.to configStackRoot
1329+
pure (sr </> [reldir|hooks|])
1330+
1331+
-- | @STACK_ROOT\/hooks\/ghc-install.sh@
1332+
ghcInstallHook :: HasConfig env => RIO env (Path Abs File)
1333+
ghcInstallHook = do
1334+
hd <- hooksDir
1335+
pure (hd </> [relfile|ghc-install.sh|])
1336+
13231337
-- | Per-project work dir
13241338
getProjectWorkDir :: (HasBuildConfig env, MonadReader env m) => m (Path Abs Dir)
13251339
getProjectWorkDir = do
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import System.Process (rawSystem)
2+
import Control.Exception (throwIO)
3+
import StackTest
4+
import Control.Monad (unless)
5+
6+
main :: IO ()
7+
main = rawSystem "sh" ["run.sh"] >>= throwIO
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/fake-root/
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
main = putStrLn "Looks like everything is working!"
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
#!/usr/bin/env sh
2+
3+
set -exu
4+
5+
stack_bin=$("$STACK_EXE" path --resolver ghc-8.6.5 --compiler-bin)
6+
7+
export STACK_ROOT=$(pwd)/fake-root
8+
9+
mkdir -p "${STACK_ROOT}"/hooks
10+
11+
echo "echo '${stack_bin}/ghc'" > "${STACK_ROOT}"/hooks/ghc-install.sh
12+
chmod +x "${STACK_ROOT}"/hooks/ghc-install.sh
13+
14+
"$STACK_EXE" --no-install-ghc --resolver ghc-8.6.5 ghc -- --info
15+
"$STACK_EXE" --no-install-ghc --resolver ghc-8.6.5 runghc foo.hs

0 commit comments

Comments
 (0)