Skip to content

Commit 25d97f8

Browse files
Use response files for ghc invocations
Before this change, `cabal` could fail with the following error message when building very large Haskell packages: ``` ghc: createProcess: posix_spawnp: resource exhausted (Argument list too long) ``` This is because when the number of modules or dependencies grows large enough, then the `ghc` command line can potentially exceed the `ARG_MAX` command line length limit. However, `ghc` supports response files in order to work around these sorts of command line length limitations, so this change enables the use of those response files. Note that this requires taking a special precaution to not pass RTS options to the response file because there's no way that `ghc` can support RTS options via the response file. The reason why is because the Haskell runtime processes these options (not `ghc`), so if you store the RTS options in the response file then `ghc`'s command line parser won't know what to do with them. This means that `ghc` commands can still potentially fail if the RTS options get long enough, but this is less likely to occur in practice since RTS options tend to be significantly smaller than non-RTS options. This also requires skipping the response file if the first argument is `--interactive`. See the corresponding code comment which explains why in more detail. Co-Authored-By: Gabriella Gonzales <[email protected]>
1 parent 78982b2 commit 25d97f8

File tree

5 files changed

+183
-30
lines changed

5 files changed

+183
-30
lines changed

Cabal/src/Distribution/Simple/GHC.hs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -627,6 +627,8 @@ startInterpreter verbosity progdb comp platform packageDBs = do
627627
}
628628
checkPackageDbStack verbosity comp packageDBs
629629
(ghcProg, _) <- requireProgram verbosity ghcProgram progdb
630+
-- This doesn't pass source file arguments to GHC, so we don't have to worry
631+
-- about using a response file here.
630632
runGHC verbosity ghcProg comp platform Nothing replOpts
631633

632634
-- -----------------------------------------------------------------------------

Cabal/src/Distribution/Simple/GHC/Build/ExtraSources.hs

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import Distribution.Simple.GHC.Build.Modules
2323
import Distribution.Simple.GHC.Build.Utils
2424
import Distribution.Simple.LocalBuildInfo
2525
import Distribution.Simple.Program.Types
26+
import Distribution.Simple.Setup.Common (commonSetupTempFileOptions)
2627
import Distribution.System (Arch (JavaScript), Platform (..))
2728
import Distribution.Types.ComponentLocalBuildInfo
2829
import Distribution.Utils.Path
@@ -176,7 +177,19 @@ buildExtraSources
176177
sources = viewSources (targetComponent targetInfo)
177178
comp = compiler lbi
178179
platform = hostPlatform lbi
179-
runGhcProg = runGHC verbosity ghcProg comp platform
180+
responseFileDir = coerceSymbolicPath buildTargetDir
181+
tempFileOptions = commonSetupTempFileOptions $ buildingWhatCommonFlags buildingWhat
182+
runGhcProg =
183+
runGHCWithResponseFile
184+
"ghc.rsp"
185+
Nothing
186+
responseFileDir
187+
tempFileOptions
188+
verbosity
189+
ghcProg
190+
comp
191+
platform
192+
mbWorkDir
180193

181194
buildAction :: SymbolicPath Pkg File -> IO ()
182195
buildAction sourceFile = do
@@ -219,7 +232,7 @@ buildExtraSources
219232
compileIfNeeded :: GhcOptions -> IO ()
220233
compileIfNeeded opts = do
221234
needsRecomp <- checkNeedsRecompilation mbWorkDir sourceFile opts
222-
when needsRecomp $ runGhcProg mbWorkDir opts
235+
when needsRecomp $ runGhcProg opts
223236

224237
createDirectoryIfMissingVerbose verbosity True (i odir)
225238
case targetComponent targetInfo of
@@ -251,6 +264,7 @@ buildExtraSources
251264
DynWay -> compileIfNeeded sharedSrcOpts
252265
ProfWay -> compileIfNeeded profSrcOpts
253266
ProfDynWay -> compileIfNeeded profSharedSrcOpts
267+
254268
-- build any sources
255269
if (null sources || componentIsIndefinite clbi)
256270
then return mempty

Cabal/src/Distribution/Simple/GHC/Build/Link.hs

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,8 @@ linkOrLoadComponent
9898
clbi = buildCLBI pbci
9999
isIndef = componentIsIndefinite clbi
100100
mbWorkDir = mbWorkDirLBI lbi
101+
responseFileDir = coerceSymbolicPath buildTargetDir
102+
tempFileOptions = commonSetupTempFileOptions $ buildingWhatCommonFlags what
101103

102104
-- See Note [Symbolic paths] in Distribution.Utils.Path
103105
i = interpretSymbolicPathLBI lbi
@@ -188,10 +190,27 @@ linkOrLoadComponent
188190
-- exports.
189191
when (case component of CLib lib -> null (allLibModules lib clbi); _ -> False) $
190192
warn verbosity "No exposed modules"
191-
runReplOrWriteFlags ghcProg lbi replFlags replOpts_final (pkgName (PD.package pkg_descr)) target
193+
runReplOrWriteFlags
194+
ghcProg
195+
lbi
196+
replFlags
197+
replOpts_final
198+
(pkgName (PD.package pkg_descr))
199+
target
200+
responseFileDir
192201
_otherwise ->
193202
let
194-
runGhcProg = runGHC verbosity ghcProg comp platform mbWorkDir
203+
runGhcProg =
204+
runGHCWithResponseFile
205+
"ghc.rsp"
206+
Nothing
207+
responseFileDir
208+
tempFileOptions
209+
verbosity
210+
ghcProg
211+
comp
212+
platform
213+
mbWorkDir
195214
platform = hostPlatform lbi
196215
comp = compiler lbi
197216
get_rpaths ways =
@@ -721,17 +740,30 @@ runReplOrWriteFlags
721740
-> GhcOptions
722741
-> PackageName
723742
-> TargetInfo
743+
-> SymbolicPath Pkg (Dir Response)
724744
-> IO ()
725-
runReplOrWriteFlags ghcProg lbi rflags ghcOpts pkg_name target =
745+
runReplOrWriteFlags ghcProg lbi rflags ghcOpts pkg_name target responseFileDir =
726746
let bi = componentBuildInfo $ targetComponent target
727747
clbi = targetCLBI target
728748
comp = compiler lbi
729749
platform = hostPlatform lbi
730750
common = configCommonFlags $ configFlags lbi
731751
mbWorkDir = mbWorkDirLBI lbi
732752
verbosity = fromFlag $ setupVerbosity common
753+
tempFileOptions = commonSetupTempFileOptions common
733754
in case replOptionsFlagOutput (replReplOptions rflags) of
734-
NoFlag -> runGHC verbosity ghcProg comp platform mbWorkDir ghcOpts
755+
NoFlag ->
756+
runGHCWithResponseFile
757+
"ghc.rsp"
758+
Nothing
759+
responseFileDir
760+
tempFileOptions
761+
verbosity
762+
ghcProg
763+
comp
764+
platform
765+
mbWorkDir
766+
ghcOpts
735767
Flag out_dir -> do
736768
let uid = componentUnitId clbi
737769
this_unit = prettyShow uid

Cabal/src/Distribution/Simple/GHC/Build/Modules.hs

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -137,20 +137,31 @@ buildHaskellModules numJobs ghcProg mbMainFile inputModules buildTargetDir neede
137137
| BuildRepl{} <- what = True
138138
| otherwise = False
139139

140-
-- TODO: do we need to put hs-boot files into place for mutually recursive
141-
-- modules? FIX: what about exeName.hi-boot?
140+
-- TODO: do we need to put hs-boot files into place for mutually recursive
141+
-- modules? FIX: what about exeName.hi-boot?
142142

143-
-- Determine if program coverage should be enabled and if so, what
144-
-- '-hpcdir' should be.
145-
let isCoverageEnabled = if isLib then libCoverage lbi else exeCoverage lbi
146-
hpcdir way
147-
| forRepl = mempty -- HPC is not supported in ghci
148-
| isCoverageEnabled = Flag $ Hpc.mixDir (coerceSymbolicPath $ coerceSymbolicPath buildTargetDir </> extraCompilationArtifacts) way
149-
| otherwise = mempty
143+
-- Determine if program coverage should be enabled and if so, what
144+
-- '-hpcdir' should be.
145+
isCoverageEnabled = if isLib then libCoverage lbi else exeCoverage lbi
146+
hpcdir way
147+
| forRepl = mempty -- HPC is not supported in ghci
148+
| isCoverageEnabled = Flag $ Hpc.mixDir (coerceSymbolicPath $ coerceSymbolicPath buildTargetDir </> extraCompilationArtifacts) way
149+
| otherwise = mempty
150150

151-
let
152151
mbWorkDir = mbWorkDirLBI lbi
153-
runGhcProg = runGHC verbosity ghcProg comp platform mbWorkDir
152+
responseFileDir = coerceSymbolicPath buildTargetDir
153+
tempFileOptions = commonSetupTempFileOptions $ buildingWhatCommonFlags what
154+
runGhcProg =
155+
runGHCWithResponseFile
156+
"ghc.rsp"
157+
Nothing
158+
responseFileDir
159+
tempFileOptions
160+
verbosity
161+
ghcProg
162+
comp
163+
platform
164+
mbWorkDir
154165
platform = hostPlatform lbi
155166

156167
(hsMains, scriptMains) =

Cabal/src/Distribution/Simple/Program/GHC.hs

Lines changed: 107 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ module Distribution.Simple.Program.GHC
1616
, ghcInvocation
1717
, renderGhcOptions
1818
, runGHC
19+
, runGHCWithResponseFile
1920
, packageDbArgsDb
2021
, normaliseGhcArgs
2122
) where
@@ -32,8 +33,10 @@ import Distribution.Simple.Compiler
3233
import Distribution.Simple.Flag
3334
import Distribution.Simple.GHC.ImplInfo
3435
import Distribution.Simple.Program.Find (getExtraPathEnv)
36+
import Distribution.Simple.Program.ResponseFile
3537
import Distribution.Simple.Program.Run
3638
import Distribution.Simple.Program.Types
39+
import Distribution.Simple.Utils (TempFileOptions, infoNoWrap)
3740
import Distribution.System
3841
import Distribution.Types.ComponentId
3942
import Distribution.Types.ParStrat
@@ -42,17 +45,19 @@ import Distribution.Utils.Path
4245
import Distribution.Verbosity
4346
import Distribution.Version
4447

48+
import GHC.IO.Encoding (TextEncoding)
4549
import Language.Haskell.Extension
4650

4751
import Data.List (stripPrefix)
4852
import qualified Data.Map as Map
4953
import Data.Monoid (All (..), Any (..), Endo (..))
5054
import qualified Data.Set as Set
55+
import qualified System.Process as Process
5156

5257
normaliseGhcArgs :: Maybe Version -> PackageDescription -> [String] -> [String]
5358
normaliseGhcArgs (Just ghcVersion) PackageDescription{..} ghcArgs
5459
| ghcVersion `withinRange` supportedGHCVersions =
55-
argumentFilters . filter simpleFilters . filterRtsOpts $ ghcArgs
60+
argumentFilters . filter simpleFilters . filterRtsArgs $ ghcArgs
5661
where
5762
supportedGHCVersions :: VersionRange
5863
supportedGHCVersions = orLaterVersion (mkVersion [8, 0])
@@ -162,18 +167,9 @@ normaliseGhcArgs (Just ghcVersion) PackageDescription{..} ghcArgs
162167
flagArgumentFilter
163168
["-ghci-script", "-H", "-interactive-print"]
164169

165-
filterRtsOpts :: [String] -> [String]
166-
filterRtsOpts = go False
167-
where
168-
go :: Bool -> [String] -> [String]
169-
go _ [] = []
170-
go _ ("+RTS" : opts) = go True opts
171-
go _ ("-RTS" : opts) = go False opts
172-
go isRTSopts (opt : opts) = addOpt $ go isRTSopts opts
173-
where
174-
addOpt
175-
| isRTSopts = id
176-
| otherwise = (opt :)
170+
-- \| Remove RTS arguments from a list.
171+
filterRtsArgs :: [String] -> [String]
172+
filterRtsArgs = snd . splitRTSArgs
177173

178174
simpleFilters :: String -> Bool
179175
simpleFilters =
@@ -646,6 +642,84 @@ runGHC verbosity ghcProg comp platform mbWorkDir opts = do
646642
runProgramInvocation verbosity
647643
=<< ghcInvocation verbosity ghcProg comp platform mbWorkDir opts
648644

645+
runGHCWithResponseFile
646+
:: FilePath
647+
-> Maybe TextEncoding
648+
-> SymbolicPath Pkg (Dir Response)
649+
-> TempFileOptions
650+
-> Verbosity
651+
-> ConfiguredProgram
652+
-> Compiler
653+
-> Platform
654+
-> Maybe (SymbolicPath CWD (Dir Pkg))
655+
-> GhcOptions
656+
-> IO ()
657+
runGHCWithResponseFile fileNameTemplate encoding responseFileDir tempFileOptions verbosity ghcProg comp platform maybeWorkDir opts = do
658+
invocation <- ghcInvocation verbosity ghcProg comp platform maybeWorkDir opts
659+
660+
let compilerSupportsResponseFiles =
661+
case compilerCompatVersion GHC comp of
662+
-- GHC 9.4 is the first version which supports response files.
663+
Just version -> version >= mkVersion [9, 4]
664+
Nothing -> False
665+
666+
args = progInvokeArgs invocation
667+
668+
-- Don't use response files if the first argument is `--interactive`, for
669+
-- two related reasons.
670+
--
671+
-- `hie-bios` relies on a hack to intercept the command-line that `Cabal`
672+
-- supplies to `ghc`. Specifically, `hie-bios` creates a script around
673+
-- `ghc` that detects if the first option is `--interactive` and if so then
674+
-- instead of running `ghc` it prints the command-line that `ghc` was given
675+
-- instead of running the command:
676+
--
677+
-- https://github.com/haskell/hie-bios/blob/ce863dba7b57ded20160b4f11a487e4ff8372c08/wrappers/cabal#L7
678+
--
679+
-- … so we can't store that flag in the response file, otherwise that will
680+
-- break. However, even if we were to add a special-case to keep that flag
681+
-- out of the response file things would still break because `hie-bios`
682+
-- stores the arguments to `ghc` that the wrapper script outputs and reuses
683+
-- them later. That breaks if you use a response file because it will
684+
-- store an argument like `@…/ghc36000-0.rsp` which is a temporary path
685+
-- that no longer exists after the wrapper script completes.
686+
--
687+
-- The work-around here is that we don't use a response file at all if the
688+
-- first argument (and only the first argument) to `ghc` is
689+
-- `--interactive`. This ensures that `hie-bios` and all downstream
690+
-- utilities (e.g. `haskell-language-server`) continue working.
691+
--
692+
--
693+
useResponseFile =
694+
case args of
695+
"--interactive" : _ -> False
696+
_ -> compilerSupportsResponseFiles
697+
698+
if not useResponseFile
699+
then runProgramInvocation verbosity invocation
700+
else do
701+
let (rtsArgs, otherArgs) = splitRTSArgs args
702+
703+
withResponseFile
704+
verbosity
705+
tempFileOptions
706+
maybeWorkDir
707+
responseFileDir
708+
fileNameTemplate
709+
encoding
710+
otherArgs
711+
$ \responseFile -> do
712+
let newInvocation =
713+
invocation{progInvokeArgs = ('@' : responseFile) : rtsArgs}
714+
715+
infoNoWrap verbosity $
716+
"GHC response file arguments: "
717+
<> case otherArgs of
718+
[] -> ""
719+
arg : args' -> Process.showCommandForUser arg args'
720+
721+
runProgramInvocation verbosity newInvocation
722+
649723
ghcInvocation
650724
:: Verbosity
651725
-> ConfiguredProgram
@@ -959,6 +1033,26 @@ packageDbArgs implInfo
9591033
| flagPackageConf implInfo = packageDbArgsConf
9601034
| otherwise = packageDbArgsDb
9611035

1036+
-- | Split a list of command-line arguments into RTS arguments and non-RTS
1037+
-- arguments.
1038+
splitRTSArgs :: [String] -> ([String], [String])
1039+
splitRTSArgs args =
1040+
let addRTSArg arg ~(rtsArgs, nonRTSArgs) = (arg : rtsArgs, nonRTSArgs)
1041+
addNonRTSArg arg ~(rtsArgs, nonRTSArgs) = (rtsArgs, arg : nonRTSArgs)
1042+
1043+
go _ [] = ([], [])
1044+
go isRTSArg (arg : rest) =
1045+
case arg of
1046+
"+RTS" -> addRTSArg arg $ go True rest
1047+
"-RTS" -> addRTSArg arg $ go False rest
1048+
"--RTS" -> ([arg], rest)
1049+
"--" -> ([], arg : rest)
1050+
_ ->
1051+
if isRTSArg
1052+
then addRTSArg arg $ go isRTSArg rest
1053+
else addNonRTSArg arg $ go isRTSArg rest
1054+
in go False args
1055+
9621056
-- -----------------------------------------------------------------------------
9631057
-- Boilerplate Monoid instance for GhcOptions
9641058

0 commit comments

Comments
 (0)