Skip to content

Commit 6549f2d

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 e8d73a2 commit 6549f2d

File tree

6 files changed

+191
-29
lines changed

6 files changed

+191
-29
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: 14 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,17 @@ buildExtraSources
176177
sources = viewSources (targetComponent targetInfo)
177178
comp = compiler lbi
178179
platform = hostPlatform lbi
179-
runGhcProg = runGHC verbosity ghcProg comp platform
180+
tempFileOptions = commonSetupTempFileOptions $ buildingWhatCommonFlags buildingWhat
181+
runGhcProg =
182+
runGHCWithResponseFile
183+
"ghc.rsp"
184+
Nothing
185+
tempFileOptions
186+
verbosity
187+
ghcProg
188+
comp
189+
platform
190+
mbWorkDir
180191

181192
buildAction :: SymbolicPath Pkg File -> IO ()
182193
buildAction sourceFile = do
@@ -219,7 +230,7 @@ buildExtraSources
219230
compileIfNeeded :: GhcOptions -> IO ()
220231
compileIfNeeded opts = do
221232
needsRecomp <- checkNeedsRecompilation mbWorkDir sourceFile opts
222-
when needsRecomp $ runGhcProg mbWorkDir opts
233+
when needsRecomp $ runGhcProg opts
223234

224235
createDirectoryIfMissingVerbose verbosity True (i odir)
225236
case targetComponent targetInfo of
@@ -251,6 +262,7 @@ buildExtraSources
251262
DynWay -> compileIfNeeded sharedSrcOpts
252263
ProfWay -> compileIfNeeded profSrcOpts
253264
ProfDynWay -> compileIfNeeded profSharedSrcOpts
265+
254266
-- build any sources
255267
if (null sources || componentIsIndefinite clbi)
256268
then return mempty

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

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ linkOrLoadComponent
9898
clbi = buildCLBI pbci
9999
isIndef = componentIsIndefinite clbi
100100
mbWorkDir = mbWorkDirLBI lbi
101+
tempFileOptions = commonSetupTempFileOptions $ buildingWhatCommonFlags what
101102

102103
-- See Note [Symbolic paths] in Distribution.Utils.Path
103104
i = interpretSymbolicPathLBI lbi
@@ -188,10 +189,25 @@ linkOrLoadComponent
188189
-- exports.
189190
when (case component of CLib lib -> null (allLibModules lib clbi); _ -> False) $
190191
warn verbosity "No exposed modules"
191-
runReplOrWriteFlags ghcProg lbi replFlags replOpts_final (pkgName (PD.package pkg_descr)) target
192+
runReplOrWriteFlags
193+
ghcProg
194+
lbi
195+
replFlags
196+
replOpts_final
197+
(pkgName (PD.package pkg_descr))
198+
target
192199
_otherwise ->
193200
let
194-
runGhcProg = runGHC verbosity ghcProg comp platform mbWorkDir
201+
runGhcProg =
202+
runGHCWithResponseFile
203+
"ghc.rsp"
204+
Nothing
205+
tempFileOptions
206+
verbosity
207+
ghcProg
208+
comp
209+
platform
210+
mbWorkDir
195211
platform = hostPlatform lbi
196212
comp = compiler lbi
197213
get_rpaths ways =
@@ -730,8 +746,19 @@ runReplOrWriteFlags ghcProg lbi rflags ghcOpts pkg_name target =
730746
common = configCommonFlags $ configFlags lbi
731747
mbWorkDir = mbWorkDirLBI lbi
732748
verbosity = fromFlag $ setupVerbosity common
749+
tempFileOptions = commonSetupTempFileOptions common
733750
in case replOptionsFlagOutput (replReplOptions rflags) of
734-
NoFlag -> runGHC verbosity ghcProg comp platform mbWorkDir ghcOpts
751+
NoFlag ->
752+
runGHCWithResponseFile
753+
"ghc.rsp"
754+
Nothing
755+
tempFileOptions
756+
verbosity
757+
ghcProg
758+
comp
759+
platform
760+
mbWorkDir
761+
ghcOpts
735762
Flag out_dir -> do
736763
let uid = componentUnitId clbi
737764
this_unit = prettyShow uid

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

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -137,20 +137,29 @@ 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+
tempFileOptions = commonSetupTempFileOptions $ buildingWhatCommonFlags what
153+
runGhcProg =
154+
runGHCWithResponseFile
155+
"ghc.rsp"
156+
Nothing
157+
tempFileOptions
158+
verbosity
159+
ghcProg
160+
comp
161+
platform
162+
mbWorkDir
154163
platform = hostPlatform lbi
155164

156165
(hsMains, scriptMains) =

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

Lines changed: 104 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 =
@@ -647,6 +643,81 @@ runGHC verbosity ghcProg comp platform mbWorkDir opts = do
647643
runProgramInvocation verbosity
648644
=<< ghcInvocation verbosity ghcProg comp platform mbWorkDir opts
649645

646+
runGHCWithResponseFile
647+
:: FilePath
648+
-> Maybe TextEncoding
649+
-> TempFileOptions
650+
-> Verbosity
651+
-> ConfiguredProgram
652+
-> Compiler
653+
-> Platform
654+
-> Maybe (SymbolicPath CWD (Dir Pkg))
655+
-> GhcOptions
656+
-> IO ()
657+
runGHCWithResponseFile fileNameTemplate encoding 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+
fileNameTemplate
707+
encoding
708+
otherArgs
709+
$ \responseFile -> do
710+
let newInvocation =
711+
invocation{progInvokeArgs = ('@' : responseFile) : rtsArgs}
712+
713+
infoNoWrap verbosity $
714+
"GHC response file arguments: "
715+
<> case otherArgs of
716+
[] -> ""
717+
arg : args' -> Process.showCommandForUser arg args'
718+
719+
runProgramInvocation verbosity newInvocation
720+
650721
ghcInvocation
651722
:: Verbosity
652723
-> ConfiguredProgram
@@ -960,6 +1031,26 @@ packageDbArgs implInfo
9601031
| flagPackageConf implInfo = packageDbArgsConf
9611032
| otherwise = packageDbArgsDb
9621033

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

changelog.d/pr-9367

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
synopsis: "Use response files when calling `ghc`"
3+
packages: [Cabal]
4+
prs: 9367
5+
---
6+
7+
Cabal now passes GHC arguments through a response file. This prevents an error
8+
where very large packages would fail to build due to the module names exceeding
9+
the `ARG_MAX` command-line length limit:
10+
11+
```
12+
ghc: createProcess: posix_spawnp: resource exhausted (Argument list too long)
13+
```
14+
15+
Notes:
16+
- Response files will not be used if the first argument to `ghc` is
17+
`--interactive`.
18+
- RTS options (like `+RTS -H32m -S -RTS`) will not be included in response
19+
files.
20+
- For reproducing commands after Cabal exits, `--keep-temp-files` can be used
21+
to prevent the response files from being deleted.

0 commit comments

Comments
 (0)