use the GHC API to figure out the package configuration for yesod devel

This commit is contained in:
Luite Stegeman 2013-02-19 04:07:22 +01:00
parent c39fa8ddf8
commit 4013ef6160
2 changed files with 101 additions and 143 deletions

View File

@ -9,18 +9,11 @@ module Devel
import qualified Distribution.Compiler as D
import qualified Distribution.InstalledPackageInfo as IPI
import qualified Distribution.ModuleName as D
import qualified Distribution.Package as D
import qualified Distribution.PackageDescription as D
import qualified Distribution.PackageDescription.Parse as D
import qualified Distribution.Simple.Build as D
import qualified Distribution.Simple.Compiler as D
import qualified Distribution.Simple.Configure as D
import qualified Distribution.Simple.LocalBuildInfo as D
import qualified Distribution.Simple.Program as D
import qualified Distribution.Simple.Register as D
import qualified Distribution.Simple.Setup as DSS
import qualified Distribution.Simple.Utils as D
import qualified Distribution.Verbosity as D
@ -40,9 +33,6 @@ import qualified Data.List as L
import qualified Data.Map as Map
import Data.Maybe (fromMaybe)
import qualified Data.Set as Set
import qualified Data.Text as T
import Data.Text.Encoding (decodeUtf8With)
import Data.Text.Encoding.Error (lenientDecode)
import System.Directory
import System.Environment (getEnvironment)
@ -53,6 +43,7 @@ import System.FilePath (dropExtension,
splitDirectories,
takeExtension, (</>))
import System.FSNotify
import System.IO (Handle)
import System.IO.Error (isDoesNotExistError)
import System.Posix.Types (EpochTime)
import System.PosixCompat.Files (getFileStatus,
@ -66,9 +57,9 @@ import System.Process (ProcessHandle,
import System.Timeout (timeout)
import Build (getDeps, isNewerThan,
recompDeps, safeReadFile)
recompDeps)
import GhcBuild (buildPackage,
getBuildFlags)
getBuildFlags, getPackageArgs)
import qualified Config as GHC
import Data.Conduit.Network (HostPreference (HostIPv4),
@ -116,6 +107,10 @@ getBuildDir opts = fromMaybe "dist" (buildDir opts)
defaultDevelOpts :: DevelOpts
defaultDevelOpts = DevelOpts False False False (-1) Nothing Nothing Nothing 3000 10
cabalProgram :: DevelOpts -> FilePath
cabalProgram opts | isCabalDev opts = "cabal-dev"
| otherwise = "cabal"
-- | Run a reverse proxy from port 3000 to 3001. If there is no response on
-- 3001, give an appropriate message to the user.
reverseProxy :: DevelOpts -> I.IORef Int -> IO ()
@ -183,26 +178,34 @@ devel opts passThroughArgs = withSocketsDo $ withManager $ \manager -> do
cabal <- liftIO $ D.findPackageDesc "."
gpd <- liftIO $ D.readPackageDescription D.normal cabal
ldar <- liftIO lookupLdAr
(hsSourceDirs, lib) <- liftIO $ checkCabalFile gpd
(hsSourceDirs, _) <- liftIO $ checkCabalFile gpd
liftIO $ removeFileIfExists (bd </> "setup-config")
liftIO $ configure cabal ghcVer gpd opts
liftIO $ removeFileIfExists "yesod-devel/ghcargs.txt" -- these files contain the wrong data after
liftIO $ removeFileIfExists "yesod-devel/arargs.txt" -- the configure step, remove them to force
liftIO $ removeFileIfExists "yesod-devel/ldargs.txt" -- a cabal build first
rebuild <- liftIO $ mkRebuild gpd ghcVer cabal opts ldar
mainInnerLoop iappPort hsSourceDirs filesModified cabal gpd lib ghcVer rebuild
c <- liftIO $ configure opts passThroughArgs
if c then do
-- these files contain the wrong data after the configure step,
-- remove them to force a cabal build first
liftIO $ mapM_ removeFileIfExists [ "yesod-devel/ghcargs.txt"
, "yesod-devel/arargs.txt"
, "yesod-devel/ldargs.txt"
]
rebuild <- liftIO $ mkRebuild ghcVer cabal opts ldar
mainInnerLoop iappPort hsSourceDirs filesModified cabal rebuild
else do
liftIO (threadDelay 5000000)
mainOuterLoop iappPort filesModified
-- inner loop rebuilds after files change
mainInnerLoop iappPort hsSourceDirs filesModified cabal gpd lib ghcVer rebuild = go
mainInnerLoop iappPort hsSourceDirs filesModified cabal rebuild = go
where
go = do
_ <- recompDeps hsSourceDirs
list <- liftIO $ getFileList hsSourceDirs [cabal]
success <- liftIO rebuild
pkgArgs <- liftIO $ ghcPackageArgs opts ghcVer (D.packageDescription gpd) lib
let devArgs = pkgArgs ++ ["devel.hs"] ++ passThroughArgs
pkgArgs <- liftIO ghcPackageArgs
let devArgs = pkgArgs ++ ["devel.hs"]
let loop list0 = do
(haskellFileChanged, list1) <- liftIO $ watchForChanges filesModified hsSourceDirs [cabal] list0 (eventTimeout opts)
(haskellFileChanged, list1) <- liftIO $
watchForChanges filesModified hsSourceDirs [cabal] list0 (eventTimeout opts)
anyTouched <- recompDeps hsSourceDirs
unless (anyTouched || haskellFileChanged) $ loop list1
if not success
@ -249,53 +252,20 @@ runBuildHook (Just s) = do
runBuildHook Nothing = return ()
{-
configure with the built-in Cabal lib for non-cabal-dev, since
otherwise we cannot read the configuration later
cabal-dev uses the command-line tool, we can fall back to
cabal-dev buildopts if required
run `cabal configure' with our wrappers
-}
configure :: FilePath -> String -> D.GenericPackageDescription -> DevelOpts -> IO ()
configure _cabalFile ghcVer gpd opts = do
lbi <- D.configure (gpd, hookedBuildInfo) configFlags
D.writePersistBuildConfig (getBuildDir opts) lbi -- fixme we could keep this in memory instead of file
where
hookedBuildInfo = (Nothing, [])
configFlags0 | forceCabal opts = config
| otherwise = config
{ DSS.configProgramPaths =
[ ("ar", "yesod-ar-wrapper")
, ("ld", "yesod-ld-wrapper")
, ("ghc", "yesod-ghc-wrapper")
]
, DSS.configHcPkg = DSS.Flag "ghc-pkg"
}
#if MIN_VERSION_Cabal(1,16,0)
configFlags | isCabalDev opts = configFlags0
{ DSS.configPackageDBs =
[ Nothing
, Just D.GlobalPackageDB
, Just cabalDevPackageDb
]
}
#else
configFlags | isCabalDev opts = configFlags0
{ DSS.configPackageDB = DSS.Flag cabalDevPackageDb
}
#endif
| otherwise = configFlags0
cabalDevPackageDb = D.SpecificPackageDB ("cabal-dev/packages-" ++ ghcVer ++ ".conf")
config = (DSS.defaultConfigFlags D.defaultProgramConfiguration)
{ DSS.configConfigurationsFlags =
[ (D.FlagName "devel", True) -- legacy
, (D.FlagName "library-only", True)
]
, DSS.configProfLib = DSS.Flag False
, DSS.configUserInstall = DSS.Flag True
}
configure :: DevelOpts -> [String] -> IO Bool
configure opts extraArgs =
checkExit =<< (createProcess $ proc (cabalProgram opts)
([ "configure"
, "-flibrary-only"
, "-fdevel"
, "--with-ld=yesod-ld-wrapper"
, "--with-ghc=yesod-ghc-wrapper"
, "--with-ar=yesod-ar-wrapper"
, "--with-hc-pkg=ghc-pkg"
] ++ extraArgs)
)
removeFileIfExists :: FilePath -> IO ()
removeFileIfExists file = removeFile file `Ex.catch` handler
@ -304,17 +274,17 @@ removeFileIfExists file = removeFile file `Ex.catch` handler
handler e | isDoesNotExistError e = return ()
| otherwise = Ex.throw e
mkRebuild :: D.GenericPackageDescription -> String -> FilePath -> DevelOpts -> (FilePath, FilePath) -> IO (IO Bool)
mkRebuild gpd ghcVer cabalFile opts (ldPath, arPath)
| GHC.cProjectVersion /= ghcVer = failWith "Yesod has been compiled with a different GHC version, please reinstall"
| forceCabal opts = return (rebuildCabal gpd opts)
mkRebuild :: String -> FilePath -> DevelOpts -> (FilePath, FilePath) -> IO (IO Bool)
mkRebuild ghcVer cabalFile opts (ldPath, arPath)
| GHC.cProjectVersion /= ghcVer =
failWith "Yesod has been compiled with a different GHC version, please reinstall"
| forceCabal opts = return (rebuildCabal opts)
| otherwise = do
return $ do
n1 <- cabalFile `isNewerThan` "yesod-devel/ghcargs.txt"
n2 <- cabalFile `isNewerThan` "yesod-devel/arargs.txt"
n3 <- cabalFile `isNewerThan` "yesod-devel/ldargs.txt"
if n1 || n2 || n3
then rebuildCabal gpd opts
ns <- mapM (cabalFile `isNewerThan`)
[ "yesod-devel/ghcargs.txt", "yesod-devel/arargs.txt", "yesod-devel/ldargs.txt" ]
if or ns
then rebuildCabal opts
else do
bf <- getBuildFlags
rebuildGhc bf ldPath arPath
@ -325,18 +295,13 @@ rebuildGhc bf ld ar = do
putStrLn "Rebuilding application... (using GHC API)"
buildPackage bf ld ar
rebuildCabal :: D.GenericPackageDescription -> DevelOpts -> IO Bool
rebuildCabal _gpd opts = do
putStrLn $ "Rebuilding application... (using Cabal library)"
lbi <- getPersistBuildConfig opts -- fixme we could cache this from the configure step
let buildFlags | verbose opts = DSS.defaultBuildFlags
| otherwise = DSS.defaultBuildFlags { DSS.buildVerbosity = DSS.Flag D.silent }
tryBool $ D.build (D.localPkgDescr lbi) lbi buildFlags []
tryBool :: IO a -> IO Bool
tryBool a = (a >> return True) `Ex.catch` \(e::Ex.SomeException) -> do
putStrLn $ "Exception: " ++ show e
return False
rebuildCabal :: DevelOpts -> IO Bool
rebuildCabal opts = do
putStrLn $ "Rebuilding application... (using " ++ cabalProgram opts ++ ")"
checkExit =<< createProcess (proc (cabalProgram opts) args)
where
args | verbose opts = [ "build" ]
| otherwise = [ "build", "-v0" ]
try_ :: forall a. IO a -> IO ()
try_ x = (Ex.try x :: IO (Either Ex.SomeException a)) >> return ()
@ -421,57 +386,8 @@ ghcVersion = fmap getNumber $ readProcess "runghc" ["--numeric-version", "0"] []
where
getNumber = filter (\x -> isNumber x || x == '.')
ghcPackageArgs :: DevelOpts -> String -> D.PackageDescription -> D.Library -> IO [String]
ghcPackageArgs opts ghcVer cabal lib = do
lbi <- getPersistBuildConfig opts
cbi <- fromMaybeErr errCbi (D.libraryConfig lbi)
if isCabalDev opts
then return ("-hide-all-packages" : "-no-user-package-conf" : inplaceConf
: selfPkgArg lbi : cabalDevConf : depArgs lbi cbi)
else return ("-hide-all-packages" : inplaceConf : selfPkgArg lbi : depArgs lbi cbi)
where
selfPkgArg lbi = pkgArg . D.inplacePackageId . D.package . D.localPkgDescr $ lbi
pkgArg (D.InstalledPackageId pkgId) = "-package-id" ++ pkgId
depArgs lbi cbi = map pkgArg (deps lbi cbi)
deps lbi cbi = let pkgInfo = D.inplaceInstalledPackageInfo "." (getBuildDir opts) cabal lib lbi cbi
in IPI.depends $ pkgInfo
errCbi = "No library ComponentBuildInfo"
cabalDevConf = "-package-confcabal-dev/packages-" ++ ghcVer ++ ".conf"
inplaceConf = "-package-conf" ++ (getBuildDir opts</>"package.conf.inplace")
getPersistBuildConfig :: DevelOpts -> IO D.LocalBuildInfo
getPersistBuildConfig opts = fromRightErr errLbi =<< getPersistConfigLenient opts -- D.maybeGetPersistBuildConfig path
where
errLbi = "Could not read BuildInfo file: " ++ D.localBuildInfoFile (getBuildDir opts) ++
"\nMake sure that cabal-install has been compiled with the same GHC version as yesod." ++
"\nand that the Cabal library used by GHC is the same version"
-- there can be slight differences in the cabal version, ignore those when loading the file as long as we can parse it
getPersistConfigLenient :: DevelOpts -> IO (Either String D.LocalBuildInfo)
getPersistConfigLenient opts = do
let file = D.localBuildInfoFile (getBuildDir opts)
exists <- doesFileExist file
if not exists
then return (Left $ "file does not exist: " ++ file)
else do
xs <- safeReadFile file
case xs of
Left e -> return $ Left $ show e
Right bs ->
return $ case lines $ T.unpack $ decodeUtf8With lenientDecode bs of
[_,l2] -> -- two lines, header and serialized rest
case reads l2 of
[(bi,_)] -> Right bi
_ -> (Left "cannot parse contents")
_ -> (Left "not a valid header/content file")
fromMaybeErr :: String -> Maybe b -> IO b
fromMaybeErr err Nothing = failWith err
fromMaybeErr _ (Just x) = return x
fromRightErr :: String -> Either String b -> IO b
fromRightErr str (Left err) = failWith (str ++ "\n" ++ err)
fromRightErr _ (Right b) = return b
ghcPackageArgs :: IO [String]
ghcPackageArgs = getBuildFlags >>= getPackageArgs
lookupDevelLib :: D.GenericPackageDescription -> D.CondTree D.ConfVar c a -> Maybe a
lookupDevelLib gpd ct | found = Just (D.condTreeData ct)
@ -507,3 +423,7 @@ waitForProcess' pid = go
Just ec -> return ec
Nothing -> threadDelay 100000 >> go
-- | wait for process started by @createProcess@, return True for ExitSuccess
checkExit :: (Maybe Handle, Maybe Handle, Maybe Handle, ProcessHandle) -> IO Bool
checkExit (_,_,_,h) = (==ExitSuccess) <$> waitForProcess' h

View File

@ -16,7 +16,7 @@
build package with the GHC API
-}
module GhcBuild (getBuildFlags, buildPackage) where
module GhcBuild (getBuildFlags, buildPackage, getPackageArgs) where
import qualified Control.Exception as Ex
import Control.Monad (when)
@ -33,14 +33,16 @@ import DriverPhases (Phase (..), anyHsc, isHaskellSrcFilename,
import DriverPipeline (compileFile, link, linkBinary, oneShot)
import DynFlags (DynFlags, compilerInfo)
import qualified DynFlags
import qualified DynFlags as DF
import qualified GHC
import GHC.Paths (libdir)
import HscTypes (HscEnv (..), emptyHomePackageTable)
import qualified Module
import MonadUtils (liftIO)
import Panic (ghcError, panic)
import SrcLoc (Located, mkGeneralLocated)
import StaticFlags (v_Ld_inputs)
import qualified StaticFlags
import StaticFlags (v_Ld_inputs)
import System.FilePath (normalise, (</>))
import Util (consIORef, looksLikeModuleName)
@ -71,6 +73,42 @@ prependHsenvArgv argv = do
_ -> hsenvArgv ++ argv
where hsenvArgv = words $ fromMaybe "" (lookup "PACKAGE_DB_FOR_GHC" env)
-- construct a command line for loading the right packages
getPackageArgs :: [Located String] -> IO [String]
getPackageArgs argv2 = do
(mode, argv3, modeFlagWarnings) <- parseModeFlags argv2
GHC.runGhc (Just libdir) $ do
dflags0 <- GHC.getSessionDynFlags
(dflags1, _, _) <- GHC.parseDynamicFlags dflags0 argv3
let pkgFlags = map convertPkgFlag (GHC.packageFlags dflags1)
hideAll | gopt DF.Opt_HideAllPackages dflags1 = [ "-hide-all-packages"]
| otherwise = []
ownPkg = "-package" ++ Module.packageIdString (DF.thisPackage dflags1)
return (extra dflags1 ++ hideAll ++ pkgFlags ++ [ownPkg])
where
convertPkgFlag (DF.ExposePackage p) = "-package" ++ p
convertPkgFlag (DF.ExposePackageId p) = "-package-id" ++ p
convertPkgFlag (DF.HidePackage p) = "-hide-package" ++ p
convertPkgFlag (DF.IgnorePackage p) = "-ignore-package" ++ p
convertPkgFlag (DF.TrustPackage p) = "-trust" ++ p
convertPkgFlag (DF.DistrustPackage p) ="-distrust" ++ p
#if __GLASGOW_HASKELL__ >= 705
extra df = concatMap convertExtra (extraConfs df)
extraConfs df = GHC.extraPkgConfs df []
convertExtra DF.GlobalPkgConf = [ ]
convertExtra DF.UserPkgConf = [ ]
convertExtra (DF.PkgConfFile file) = [ "-package-db" ++ file ]
#else
extra df = map ("-package-conf"++) (GHC.extraPkgConfs df)
#endif
#if __GLASGOW_HASKELL__ >= 707
gopt = DF.gopt
#else
gopt = DF.dopt
#endif
buildPackage :: [Located String] -> FilePath -> FilePath -> IO Bool
buildPackage a ld ar = buildPackage' a ld ar `Ex.catch` \e -> do
putStrLn ("exception building package: " ++ show (e :: Ex.SomeException))