Skip to content

Commit 9d9009c

Browse files
committed
Experimenting with the WIP multi ghcide branch
1 parent 17b150b commit 9d9009c

File tree

4 files changed

+275
-52
lines changed

4 files changed

+275
-52
lines changed

.gitmodules

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,5 @@
1010
# rm -rf path_to_submodule
1111
[submodule "ghcide"]
1212
path = ghcide
13-
url = https://github.com/digital-asset/ghcide.git
14-
# url = https://github.com/alanz/ghcide.git
13+
# url = https://github.com/digital-asset/ghcide.git
14+
url = https://github.com/alanz/ghcide.git

exe/Main.hs

Lines changed: 269 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
-- Copyright (c) 2019 The DAML Authors. All rights reserved.
22
-- SPDX-License-Identifier: Apache-2.0
33
{-# OPTIONS_GHC -Wno-dodgy-imports #-} -- GHC no longer exports def in GHC 8.6 and above
4+
{-# LANGUAGE CPP #-} -- To get precise GHC version
5+
{-# LANGUAGE TemplateHaskell #-}
46
{-# LANGUAGE DeriveGeneric #-}
57
{-# LANGUAGE OverloadedStrings #-}
68
{-# LANGUAGE RecordWildCards #-}
@@ -16,7 +18,10 @@ import Control.Exception
1618
import Control.Monad.Extra
1719
import Control.Monad.IO.Class
1820
import Data.Default
21+
import Data.Either
22+
import qualified Data.HashMap.Strict as HM
1923
import qualified Data.HashSet as HashSet
24+
import Data.IORef
2025
import Data.List.Extra
2126
import qualified Data.Map.Strict as Map
2227
import Data.Maybe
@@ -39,23 +44,40 @@ import Development.IDE.Types.Diagnostics
3944
import Development.IDE.Types.Location
4045
import Development.IDE.Types.Logger
4146
import Development.IDE.Types.Options
42-
import Development.Shake (Action, Rules, action)
43-
import HIE.Bios
47+
import Development.Shake (Action, action)
48+
import HIE.Bios.Environment (addCmdOpts)
4449
import qualified Language.Haskell.LSP.Core as LSP
4550
import Ide.Logger
4651
import Ide.Plugin
4752
import Ide.Plugin.Config
4853
import Language.Haskell.LSP.Messages
4954
import Language.Haskell.LSP.Types (LspId(IdInt))
50-
import RuleTypes
51-
import Rules
55+
import Linker (initDynLinker)
56+
import Module
57+
import NameCache
58+
import Packages
59+
-- import Paths_ghcide
5260
import qualified System.Directory.Extra as IO
5361
-- import System.Environment
5462
import System.Exit
5563
import System.FilePath
5664
import System.IO
5765
import System.Log.Logger as L
5866
import System.Time.Extra
67+
--import Outputable (pprTraceM, ppr, text)
68+
import qualified Crypto.Hash.SHA1 as H
69+
import qualified Data.ByteString.Char8 as B
70+
import Data.ByteString.Base16 (encode)
71+
import DynFlags (gopt_set, gopt_unset,
72+
updOptLevel)
73+
import GhcMonad
74+
import HscTypes (HscEnv(..), ic_dflags)
75+
import DynFlags (PackageFlag(..), PackageArg(..))
76+
import GHC hiding (def)
77+
import qualified GHC.Paths
78+
import HIE.Bios.Cradle
79+
import HIE.Bios.Types
80+
import System.Directory
5981

6082
-- ---------------------------------------------------------------------
6183

@@ -161,9 +183,10 @@ main = do
161183
{ optReportProgress = clientSupportsProgress caps
162184
, optShakeProfiling = argsShakeProfiling
163185
, optTesting = argsTesting
186+
, optInterfaceLoadingDiagnostics = argsTesting
164187
}
165188
debouncer <- newAsyncDebouncer
166-
initialise caps (cradleRules >> mainRule >> pluginRules plugins >> action kick)
189+
initialise caps (mainRule >> pluginRules plugins >> action kick)
167190
getLspId event hlsLogger debouncer options vfs
168191
else do
169192
-- GHC produces messages with UTF8 in them, so make sure the terminal doesn't error
@@ -184,44 +207,21 @@ main = do
184207
let ucradles = nubOrd cradles
185208
let n = length ucradles
186209
putStrLn $ "Found " ++ show n ++ " cradle" ++ ['s' | n /= 1]
187-
sessions <- forM (zipFrom (1 :: Int) ucradles) $ \(i, x) -> do
188-
let msg = maybe ("Implicit cradle for " ++ dir) ("Loading " ++) x
189-
putStrLn $ "\nStep 3/6, Cradle " ++ show i ++ "/" ++ show n ++ ": " ++ msg
190-
cradle <- maybe (loadImplicitCradle $ addTrailingPathSeparator dir) loadCradle x
191-
when (isNothing x) $ print cradle
192-
putStrLn $ "\nStep 4/6, Cradle " ++ show i ++ "/" ++ show n ++ ": Loading GHC Session"
193-
opts <- getComponentOptions cradle
194-
createSession opts
195-
196-
putStrLn "\nStep 5/6: Initializing the IDE"
210+
putStrLn "\nStep 3/6: Initializing the IDE"
197211
vfs <- makeVFSHandle
198-
let cradlesToSessions = Map.fromList $ zip ucradles sessions
199-
let filesToCradles = Map.fromList $ zip files cradles
200-
let grab file = fromMaybe (head sessions) $ do
201-
cradle <- Map.lookup file filesToCradles
202-
Map.lookup cradle cradlesToSessions
203-
204-
let options =
205-
(defaultIdeOptions $ return $ return . grab)
206-
{ optShakeProfiling = argsShakeProfiling }
207-
ide <- initialise def (cradleRules >> mainRule) (pure $ IdInt 0) (showEvent lock) (logger Info) noopDebouncer options vfs
208-
209-
putStrLn "\nStep 6/6: Type checking the files"
212+
debouncer <- newAsyncDebouncer
213+
-- let options =
214+
-- (defaultIdeOptions $ return $ return . grab)
215+
-- { optShakeProfiling = argsShakeProfiling }
216+
let options = defaultIdeOptions $ loadSession dir
217+
ide <- initialise def mainRule (pure $ IdInt 0) (showEvent lock) (logger Info) debouncer options vfs
218+
219+
putStrLn "\nStep 4/6: Type checking the files"
210220
setFilesOfInterest ide $ HashSet.fromList $ map toNormalizedFilePath files
211-
results <- runActionSync ide $ uses TypeCheck $ map toNormalizedFilePath files
212-
let (worked, failed) = partition fst $ zip (map isJust results) files
213-
when (failed /= []) $
214-
putStr $ unlines $ "Files that failed:" : map ((++) " * " . snd) failed
215-
216-
let files xs = let n = length xs in if n == 1 then "1 file" else show n ++ " files"
217-
putStrLn $ "\nCompleted (" ++ files worked ++ " worked, " ++ files failed ++ " failed)"
218-
219-
unless (null failed) exitFailure
220-
221-
cradleRules :: Rules ()
222-
cradleRules = do
223-
loadGhcSession
224-
cradleToSession
221+
_ <- runActionSync ide $ uses TypeCheck (map toNormalizedFilePath files)
222+
-- results <- runActionSync ide $ use TypeCheck $ toNormalizedFilePath "src/Development/IDE/Core/Rules.hs"
223+
-- results <- runActionSync ide $ use TypeCheck $ toNormalizedFilePath "exe/Main.hs"
224+
return ()
225225

226226
expandFiles :: [FilePath] -> IO [FilePath]
227227
expandFiles = concatMapM $ \x -> do
@@ -248,22 +248,189 @@ showEvent lock (EventFileDiagnostics (toNormalizedFilePath -> file) diags) =
248248
withLock lock $ T.putStrLn $ showDiagnosticsColored $ map (file,ShowDiag,) diags
249249
showEvent lock e = withLock lock $ print e
250250

251+
252+
cradleToSessionOpts :: Lock -> Cradle a -> FilePath -> IO ComponentOptions
253+
cradleToSessionOpts lock cradle file = do
254+
let showLine s = putStrLn ("> " ++ s)
255+
cradleRes <- withLock lock $ mask $ \_ -> runCradle (cradleOptsProg cradle) showLine file
256+
opts <- case cradleRes of
257+
CradleSuccess r -> pure r
258+
CradleFail err -> throwIO err
259+
-- TODO Rather than failing here, we should ignore any files that use this cradle.
260+
-- That will require some more changes.
261+
CradleNone -> fail "'none' cradle is not yet supported"
262+
pure opts
263+
264+
emptyHscEnv :: IO HscEnv
265+
emptyHscEnv = do
266+
libdir <- getLibdir
267+
env <- runGhc (Just libdir) getSession
268+
initDynLinker env
269+
pure env
270+
271+
-- Convert a target to a list of potential absolute paths.
272+
-- A TargetModule can be anywhere listed by the supplied include
273+
-- directories
274+
-- A target file is a relative path but with a specific prefix so just need
275+
-- to canonicalise it.
276+
targetToFile :: [FilePath] -> TargetId -> IO [NormalizedFilePath]
277+
targetToFile is (TargetModule mod) = do
278+
let fps = [i </> (moduleNameSlashes mod) -<.> ext | ext <- exts, i <- is ]
279+
exts = ["hs", "hs-boot", "lhs"]
280+
mapM (fmap (toNormalizedFilePath) . canonicalizePath) fps
281+
targetToFile _ (TargetFile f _) = do
282+
f' <- canonicalizePath f
283+
return [(toNormalizedFilePath f')]
284+
285+
setNameCache :: IORef NameCache -> HscEnv -> HscEnv
286+
setNameCache nc hsc = hsc { hsc_NC = nc }
287+
288+
-- This is the key function which implements multi-component support. All
289+
-- components mapping to the same hie,yaml file are mapped to the same
290+
-- HscEnv which is updated as new components are discovered.
251291
loadSession :: FilePath -> Action (FilePath -> Action HscEnvEq)
252292
loadSession dir = liftIO $ do
293+
-- Mapping from hie.yaml file to HscEnv, one per hie.yaml file
294+
hscEnvs <- newVar Map.empty
295+
-- Mapping from a filepath to HscEnv
296+
fileToFlags <- newVar Map.empty
297+
-- This caches the mapping from Mod.hs -> hie.yaml
253298
cradleLoc <- memoIO $ \v -> do
254299
res <- findCradle v
255300
-- Sometimes we get C:, sometimes we get c:, and sometimes we get a relative path
256301
-- try and normalise that
257302
-- e.g. see https://github.com/digital-asset/ghcide/issues/126
258303
res' <- traverse IO.makeAbsolute res
259304
return $ normalise <$> res'
260-
let session :: Maybe FilePath -> Action HscEnvEq
261-
session file = do
262-
-- In the absence of a cradle file, just pass the directory from where to calculate an implicit cradle
263-
let cradle = toNormalizedFilePath $ fromMaybe dir file
264-
use_ LoadCradle cradle
265-
return $ \file -> session =<< liftIO (cradleLoc file)
266305

306+
-- Create a new HscEnv from a hieYaml root and a set of options
307+
-- If the hieYaml file already has an HscEnv, the new component is
308+
-- combined with the components in the old HscEnv into a new HscEnv
309+
-- which contains both.
310+
packageSetup <- return $ \(hieYaml, opts) -> do
311+
-- Parse DynFlags for the newly discovered component
312+
hscEnv <- emptyHscEnv
313+
(df, targets) <- evalGhcEnv hscEnv $ do
314+
setOptions opts (hsc_dflags hscEnv)
315+
-- Now lookup to see whether we are combining with an exisiting HscEnv
316+
-- or making a new one. The lookup returns the HscEnv and a list of
317+
-- information about other components loaded into the HscEnv
318+
-- (unitId, DynFlag, Targets)
319+
modifyVar hscEnvs $ \m -> do
320+
-- Just deps if there's already an HscEnv
321+
-- Nothing is it's the first time we are making an HscEnv
322+
let oldDeps = Map.lookup hieYaml m
323+
let -- Add the raw information about this component to the list
324+
-- We will modify the unitId and DynFlags used for
325+
-- compilation but these are the true source of
326+
-- information.
327+
new_deps = (thisInstalledUnitId df, df, targets) : maybe [] snd oldDeps
328+
-- Get all the unit-ids for things in this component
329+
inplace = map (\(a, _, _) -> a) new_deps
330+
-- Remove all inplace dependencies from package flags for
331+
-- components in this HscEnv
332+
rearrange (uid, (df, uids), ts) = (uid, (df, uids, ts))
333+
do_one (uid,df, ts) = rearrange (uid, removeInplacePackages inplace df, ts)
334+
-- All deps, but without any packages which are also loaded
335+
-- into memory
336+
new_deps' = map do_one new_deps
337+
-- Make a new HscEnv, we have to recompile everything from
338+
-- scratch again (for now)
339+
-- It's important to keep the same NameCache though for reasons
340+
-- that I do not fully understand
341+
print ("Making new HscEnv" ++ (show inplace))
342+
hscEnv <- case oldDeps of
343+
Nothing -> emptyHscEnv
344+
Just (old_hsc, _) -> setNameCache (hsc_NC old_hsc) <$> emptyHscEnv
345+
newHscEnv <-
346+
-- Add the options for the current component to the HscEnv
347+
evalGhcEnv hscEnv $ do
348+
_ <- setSessionDynFlags df
349+
getSession
350+
-- Modify the map so the hieYaml now maps to the newly created
351+
-- HscEnv
352+
-- Returns
353+
-- * the new HscEnv so it can be used to modify the
354+
-- FilePath -> HscEnv map
355+
-- * The information for the new component which caused this cache miss
356+
-- * The modified information (without -inplace flags) for
357+
-- existing packages
358+
pure (Map.insert hieYaml (newHscEnv, new_deps) m, (newHscEnv, head new_deps', tail new_deps'))
359+
360+
361+
session <- return $ \(hieYaml, opts) -> do
362+
(hscEnv, new, old_deps) <- packageSetup (hieYaml, opts)
363+
-- TODO Handle the case where there is no hie.yaml
364+
-- Make a map from unit-id to DynFlags, this is used when trying to
365+
-- resolve imports.
366+
let uids = map (\(iuid, (df, _uis, _targets)) -> (iuid, df)) (new : old_deps)
367+
368+
-- For each component, now make a new HscEnvEq which contains the
369+
-- HscEnv for the hie.yaml file but the DynFlags for that component
370+
--
371+
-- Then look at the targets for each component and create a map
372+
-- from FilePath to the HscEnv
373+
let new_cache (_iuid, (df, _uis, targets)) = do
374+
let hscEnv' = hscEnv { hsc_dflags = df
375+
, hsc_IC = (hsc_IC hscEnv) { ic_dflags = df } }
376+
377+
res <- newHscEnvEq hscEnv' uids
378+
379+
let is = importPaths df
380+
ctargets <- concatMapM (targetToFile is . targetId) targets
381+
--pprTraceM "TARGETS" (ppr (map (text . show) ctargets))
382+
let xs = map (,res) ctargets
383+
return (xs, res)
384+
385+
-- New HscEnv for the component in question
386+
(cs, res) <- new_cache new
387+
-- Modified cache targets for everything else in the hie.yaml file
388+
-- which now uses the same EPS and so on
389+
cached_targets <- concatMapM (fmap fst . new_cache) old_deps
390+
modifyVar_ fileToFlags $ \var -> do
391+
pure $ Map.insert hieYaml (HM.fromList (cs ++ cached_targets))var
392+
return res
393+
394+
lock <- newLock
395+
cradle_lock <- newLock
396+
397+
-- This caches the mapping from hie.yaml + Mod.hs -> [String]
398+
sessionOpts <- return $ \(hieYaml, file) -> do
399+
fm <- readVar fileToFlags
400+
let mv = Map.lookup hieYaml fm
401+
let v = fromMaybe HM.empty mv
402+
cfp <- liftIO $ canonicalizePath file
403+
-- We sort so exact matches come first.
404+
case HM.lookup (toNormalizedFilePath cfp) v of
405+
Just opts -> do
406+
--putStrLn $ "Cached component of " <> show file
407+
pure opts
408+
Nothing-> do
409+
putStrLn $ "Shelling out to cabal " <> show file
410+
cradle <- maybe (loadImplicitCradle $ addTrailingPathSeparator dir) loadCradle hieYaml
411+
opts <- cradleToSessionOpts cradle_lock cradle file
412+
print opts
413+
session (hieYaml, opts)
414+
return $ \file -> liftIO $ withLock lock $ do
415+
hieYaml <- cradleLoc file
416+
sessionOpts (hieYaml, file)
417+
418+
-- This function removes all the -package flags which refer to packages we
419+
-- are going to deal with ourselves. For example, if a executable depends
420+
-- on a library component, then this function will remove the library flag
421+
-- from the package flags for the executable
422+
--
423+
-- There are several places in GHC (for example the call to hptInstances in
424+
-- tcRnImports) which assume that all modules in the HPT have the same unit
425+
-- ID. Therefore we create a fake one and give them all the same unit id.
426+
removeInplacePackages :: [InstalledUnitId] -> DynFlags -> (DynFlags, [InstalledUnitId])
427+
removeInplacePackages us df = (df { packageFlags = ps
428+
, thisInstalledUnitId = fake_uid }, uids)
429+
where
430+
(uids, ps) = partitionEithers (map go (packageFlags df))
431+
fake_uid = toInstalledUnitId (stringToUnitId "fake_uid")
432+
go p@(ExposePackage _ (UnitIdArg u) _) = if (toInstalledUnitId u `elem` us) then Left (toInstalledUnitId u) else Right p
433+
go p = Right p
267434

268435
-- | Memoize an IO function, with the characteristics:
269436
--
@@ -281,3 +448,58 @@ memoIO op = do
281448
res <- onceFork $ op k
282449
return (Map.insert k res mp, res)
283450
Just res -> return (mp, res)
451+
452+
setOptions :: GhcMonad m => ComponentOptions -> DynFlags -> m (DynFlags, [Target])
453+
setOptions (ComponentOptions theOpts _) dflags = do
454+
cacheDir <- liftIO $ getCacheDir theOpts
455+
(dflags', targets) <- addCmdOpts theOpts dflags
456+
let dflags'' =
457+
-- disabled, generated directly by ghcide instead
458+
flip gopt_unset Opt_WriteInterface $
459+
-- disabled, generated directly by ghcide instead
460+
-- also, it can confuse the interface stale check
461+
dontWriteHieFiles $
462+
setHiDir cacheDir $
463+
setDefaultHieDir cacheDir $
464+
setIgnoreInterfacePragmas $
465+
setLinkerOptions $
466+
disableOptimisation dflags'
467+
-- initPackages parses the -package flags and
468+
-- sets up the visibility for each component.
469+
(final_df, _) <- liftIO $ initPackages dflags''
470+
-- let df'' = gopt_unset df' Opt_WarnIsError
471+
return (final_df, targets)
472+
473+
474+
-- we don't want to generate object code so we compile to bytecode
475+
-- (HscInterpreted) which implies LinkInMemory
476+
-- HscInterpreted
477+
setLinkerOptions :: DynFlags -> DynFlags
478+
setLinkerOptions df = df {
479+
ghcLink = LinkInMemory
480+
, hscTarget = HscNothing
481+
, ghcMode = CompManager
482+
}
483+
484+
setIgnoreInterfacePragmas :: DynFlags -> DynFlags
485+
setIgnoreInterfacePragmas df =
486+
gopt_set (gopt_set df Opt_IgnoreInterfacePragmas) Opt_IgnoreOptimChanges
487+
488+
disableOptimisation :: DynFlags -> DynFlags
489+
disableOptimisation df = updOptLevel 0 df
490+
491+
setHiDir :: FilePath -> DynFlags -> DynFlags
492+
setHiDir f d =
493+
-- override user settings to avoid conflicts leading to recompilation
494+
d { hiDir = Just f}
495+
496+
getCacheDir :: [String] -> IO FilePath
497+
getCacheDir opts = IO.getXdgDirectory IO.XdgCache (cacheDir </> opts_hash)
498+
where
499+
-- Create a unique folder per set of different GHC options, assuming that each different set of
500+
-- GHC options will create incompatible interface files.
501+
opts_hash = B.unpack $ encode $ H.finalize $ H.updates H.init (map B.pack opts)
502+
503+
-- Prefix for the cache path
504+
cacheDir :: String
505+
cacheDir = "ghcide"

0 commit comments

Comments
 (0)