stack-1.6.1: src/Stack/Script.hs
{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE CPP #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell #-}
module Stack.Script
( scriptCmd
) where
import Stack.Prelude
import qualified Data.ByteString.Char8 as S8
import qualified Data.Conduit.List as CL
import Data.List.Split (splitWhen)
import qualified Data.Map.Strict as Map
import qualified Data.Set as Set
import qualified Data.Text as T
import Path
import Path.IO
import qualified Stack.Build
import Stack.Exec
import Stack.GhcPkg (ghcPkgExeName)
import Stack.Options.ScriptParser
import Stack.Runners
import Stack.Types.BuildPlan
import Stack.Types.Compiler
import Stack.Types.Config
import Stack.Types.PackageName
import System.FilePath (dropExtension, replaceExtension)
import System.Process.Read
-- | Run a Stack Script
scriptCmd :: ScriptOpts -> GlobalOpts -> IO ()
scriptCmd opts go' = do
file <- resolveFile' $ soFile opts
let go = go'
{ globalConfigMonoid = (globalConfigMonoid go')
{ configMonoidInstallGHC = First $ Just True
}
, globalStackYaml = SYLNoConfig $ parent file
}
withBuildConfigAndLock go $ \lk -> do
-- Some warnings in case the user somehow tries to set a
-- stack.yaml location. Note that in this functions we use
-- logError instead of logWarn because, when using the
-- interpreter mode, only error messages are shown. See:
-- https://github.com/commercialhaskell/stack/issues/3007
case globalStackYaml go' of
SYLOverride fp -> logError $ T.pack
$ "Ignoring override stack.yaml file for script command: " ++ fp
SYLDefault -> return ()
SYLNoConfig _ -> assert False (return ())
config <- view configL
menv <- liftIO $ configEnvOverride config defaultEnvSettings
wc <- view $ actualCompilerVersionL.whichCompilerL
colorFlag <- appropriateGhcColorFlag
targetsSet <-
case soPackages opts of
[] -> do
-- Using the import parser
moduleInfo <- view $ loadedSnapshotL.to toModuleInfo
getPackagesFromModuleInfo moduleInfo (soFile opts)
packages -> do
let targets = concatMap wordsComma packages
targets' <- mapM parsePackageNameFromString targets
return $ Set.fromList targets'
unless (Set.null targetsSet) $ do
-- Optimization: use the relatively cheap ghc-pkg list
-- --simple-output to check which packages are installed
-- already. If all needed packages are available, we can
-- skip the (rather expensive) build call below.
bss <- sinkProcessStdout
Nothing menv (ghcPkgExeName wc)
["list", "--simple-output"] CL.consume -- FIXME use the package info from envConfigPackages, or is that crazy?
let installed = Set.fromList
$ map toPackageName
$ words
$ S8.unpack
$ S8.concat bss
if Set.null $ Set.difference (Set.map packageNameString targetsSet) installed
then logDebug "All packages already installed"
else do
logDebug "Missing packages, performing installation"
Stack.Build.build (const $ return ()) lk defaultBuildOptsCLI
{ boptsCLITargets = map packageNameText $ Set.toList targetsSet
}
let ghcArgs = concat
[ ["-hide-all-packages"]
, maybeToList colorFlag
, map (\x -> "-package" ++ x)
$ Set.toList
$ Set.insert "base"
$ Set.map packageNameString targetsSet
, case soCompile opts of
SEInterpret -> []
SECompile -> []
SEOptimize -> ["-O2"]
, map (\x -> "--ghc-arg=" ++ x) (soGhcOptions opts)
]
munlockFile lk -- Unlock before transferring control away.
case soCompile opts of
SEInterpret -> exec menv ("run" ++ compilerExeName wc)
(ghcArgs ++ toFilePath file : soArgs opts)
_ -> do
let dir = parent file
-- use sinkProcessStdout to ensure a ProcessFailed
-- exception is generated for better error messages
sinkProcessStdout
(Just dir)
menv
(compilerExeName wc)
(ghcArgs ++ [toFilePath file])
CL.sinkNull
exec menv (toExeName $ toFilePath file) (soArgs opts)
where
toPackageName = reverse . drop 1 . dropWhile (/= '-') . reverse
-- Like words, but splits on both commas and spaces
wordsComma = splitWhen (\c -> c == ' ' || c == ',')
toExeName fp =
if isWindows
then replaceExtension fp "exe"
else dropExtension fp
isWindows :: Bool
#ifdef WINDOWS
isWindows = True
#else
isWindows = False
#endif
getPackagesFromModuleInfo
:: ModuleInfo
-> FilePath -- ^ script filename
-> RIO EnvConfig (Set PackageName)
getPackagesFromModuleInfo mi scriptFP = do
(pns1, mns) <- liftIO $ parseImports <$> S8.readFile scriptFP
pns2 <-
if Set.null mns
then return Set.empty
else do
pns <- forM (Set.toList mns) $ \mn ->
case Map.lookup mn $ miModules mi of
Just pns ->
case Set.toList pns of
[] -> assert False $ return Set.empty
[pn] -> return $ Set.singleton pn
pns' -> throwString $ concat
[ "Module "
, S8.unpack $ unModuleName mn
, " appears in multiple packages: "
, unwords $ map packageNameString pns'
]
Nothing -> return Set.empty
return $ Set.unions pns `Set.difference` blacklist
return $ Set.union pns1 pns2
-- | The Stackage project introduced the concept of hidden packages,
-- to deal with conflicting module names. However, this is a
-- relatively recent addition (at time of writing). See:
-- http://www.snoyman.com/blog/2017/01/conflicting-module-names. To
-- kick this thing off a bit better, we're included a blacklist of
-- packages that should never be auto-parsed in.
blacklist :: Set PackageName
blacklist = Set.fromList
[ $(mkPackageName "async-dejafu")
, $(mkPackageName "monads-tf")
, $(mkPackageName "crypto-api")
, $(mkPackageName "fay-base")
, $(mkPackageName "hashmap")
, $(mkPackageName "hxt-unicode")
, $(mkPackageName "hledger-web")
, $(mkPackageName "plot-gtk3")
, $(mkPackageName "gtk3")
, $(mkPackageName "regex-pcre-builtin")
, $(mkPackageName "regex-compat-tdfa")
, $(mkPackageName "log")
, $(mkPackageName "zip")
, $(mkPackageName "monad-extras")
, $(mkPackageName "control-monad-free")
, $(mkPackageName "prompt")
, $(mkPackageName "kawhi")
, $(mkPackageName "language-c")
, $(mkPackageName "gl")
, $(mkPackageName "svg-tree")
, $(mkPackageName "Glob")
, $(mkPackageName "nanospec")
, $(mkPackageName "HTF")
, $(mkPackageName "courier")
, $(mkPackageName "newtype-generics")
, $(mkPackageName "objective")
, $(mkPackageName "binary-ieee754")
, $(mkPackageName "rerebase")
, $(mkPackageName "cipher-aes")
, $(mkPackageName "cipher-blowfish")
, $(mkPackageName "cipher-camellia")
, $(mkPackageName "cipher-des")
, $(mkPackageName "cipher-rc4")
, $(mkPackageName "crypto-cipher-types")
, $(mkPackageName "crypto-numbers")
, $(mkPackageName "crypto-pubkey")
, $(mkPackageName "crypto-random")
, $(mkPackageName "cryptohash")
, $(mkPackageName "cryptohash-conduit")
, $(mkPackageName "cryptohash-md5")
, $(mkPackageName "cryptohash-sha1")
, $(mkPackageName "cryptohash-sha256")
]
toModuleInfo :: LoadedSnapshot -> ModuleInfo
toModuleInfo ls =
mconcat
$ map (\(pn, lpi) ->
ModuleInfo
$ Map.fromList
$ map (\mn -> (mn, Set.singleton pn))
$ Set.toList
$ lpiExposedModules lpi)
$ filter (\(pn, lpi) ->
not (lpiHide lpi) &&
pn `Set.notMember` blacklist)
$ Map.toList
$ Map.union (void <$> lsPackages ls) (void <$> lsGlobals ls)
parseImports :: ByteString -> (Set PackageName, Set ModuleName)
parseImports =
fold . mapMaybe (parseLine . stripCR') . S8.lines
where
-- Remove any carriage return character present at the end, to
-- support Windows-style line endings (CRLF)
stripCR' bs
| S8.null bs = bs
| S8.last bs == '\r' = S8.init bs
| otherwise = bs
stripPrefix x y
| x `S8.isPrefixOf` y = Just $ S8.drop (S8.length x) y
| otherwise = Nothing
parseLine bs0 = do
bs1 <- stripPrefix "import " bs0
let bs2 = S8.dropWhile (== ' ') bs1
bs3 = fromMaybe bs2 $ stripPrefix "qualified " bs2
case stripPrefix "\"" bs3 of
Just bs4 -> do
pn <- parsePackageNameFromString $ S8.unpack $ S8.takeWhile (/= '"') bs4
Just (Set.singleton pn, Set.empty)
Nothing -> Just
( Set.empty
, Set.singleton
$ ModuleName
$ S8.takeWhile (\c -> c /= ' ' && c /= '(') bs3
)