fix building with GHC API

This commit is contained in:
Luite Stegeman 2012-04-13 02:02:58 +02:00
parent 2ba9828295
commit 2f5628d254
4 changed files with 191 additions and 75 deletions

View File

@ -3,6 +3,7 @@
{-# LANGUAGE CPP #-}
module Devel
( devel
, DevelOpts(..)
) where
@ -11,9 +12,17 @@ import qualified Distribution.Verbosity as D
import qualified Distribution.PackageDescription.Parse as D
import qualified Distribution.PackageDescription as D
import qualified Distribution.ModuleName as D
import qualified Distribution.Simple.Setup as DSS
import qualified Distribution.Simple.Configure as D
import qualified Distribution.Simple.Program as D
import qualified Distribution.Simple.Build as D
import qualified Distribution.Simple.Register as D
import qualified Distribution.Compiler as D
-- import qualified Distribution.InstalledPackageInfo as D
import qualified Distribution.InstalledPackageInfo as IPI
import qualified Distribution.Simple.LocalBuildInfo as D
import qualified Distribution.Package as D
import qualified Distribution.Verbosity as D
import Control.Applicative ((<$>), (<*>))
import Control.Concurrent (forkIO, threadDelay)
@ -24,6 +33,7 @@ import Data.Char (isUpper, isNumber)
import qualified Data.List as L
import qualified Data.Map as Map
import qualified Data.Set as Set
import Data.Maybe (fromMaybe)
import System.Directory
import System.Exit (exitFailure, exitSuccess, ExitCode (..))
@ -33,6 +43,7 @@ import System.PosixCompat.Files (modificationTime, getFileStatus)
import System.Process (createProcess, proc, terminateProcess, readProcess, ProcessHandle,
getProcessExitCode,waitForProcess, rawSystem, runInteractiveProcess)
import System.IO (hClose, hIsEOF, hGetLine, stdout, stderr, hPutStrLn)
import System.IO.Error (isDoesNotExistError)
import Build (recompDeps, getDeps, isNewerThan)
import GhcBuild (getBuildFlags, buildPackage)
@ -49,10 +60,22 @@ writeLock = do
writeFile lockFile ""
removeLock :: IO ()
removeLock = try_ (removeFile lockFile)
removeLock = removeFileIfExists lockFile
devel :: Bool -> Bool -> [String] -> IO ()
devel isCabalDev forceCabal passThroughArgs = do
data DevelOpts = DevelOpts
{ isCabalDev :: Bool
, forceCabal :: Bool
, verbose :: Bool
} deriving (Show, Eq)
cabalCommand :: DevelOpts -> FilePath
cabalCommand opts | isCabalDev opts = "cabal-dev"
| otherwise = "cabal"
defaultDevelOpts = DevelOpts False False False
devel :: DevelOpts -> [String] -> IO ()
devel opts passThroughArgs = do
checkDevelFile
writeLock
@ -62,54 +85,35 @@ devel isCabalDev forceCabal passThroughArgs = do
gpd <- D.readPackageDescription D.normal cabal
ldar <- lookupLdAr
hsSourceDirs <- checkCabalFile gpd
(hsSourceDirs, lib) <- checkCabalFile gpd
_<- rawSystem cmd args
mainLoop hsSourceDirs cabal ldar
removeFileIfExists "dist/setup-config"
configure cabal gpd opts
removeFileIfExists "dist/ghcargs.txt" -- these files contain the wrong data after
removeFileIfExists "dist/arargs.txt" -- the configure step, remove them to force
removeFileIfExists "dist/ldargs.txt" -- a cabal build first
mainLoop hsSourceDirs cabal gpd lib ldar
_ <- getLine
writeLock
exitSuccess
where
cmd | isCabalDev = "cabal-dev"
| otherwise = "cabal"
diffArgs | isCabalDev = [
"--cabal-install-arg=--with-compiler=yesod-ghc-wrapper"
, "--cabal-install-arg=--with-hc-pkg=ghc-pkg"
, "--cabal-install-arg=--with-ld=yesod-ld-wrapper"
, "--cabal-install-arg=--with-ar=yesod-ar-wrapper"
, "--cabal-install-arg=-fdevel" -- legacy
, "--cabal-install-arg=-flibrary-only"
]
| otherwise = [
"--with-compiler=yesod-ghc-wrapper"
, "--with-hc-pkg=ghc-pkg"
, "--with-ld=yesod-ld-wrapper"
, "--with-ar=yesod-ar-wrapper"
, "-fdevel" -- legacy
, "-flibrary-only"
]
args = "configure":diffArgs ++ ["--disable-library-profiling" ]
mainLoop :: [FilePath] -> FilePath -> (FilePath, FilePath) -> IO ()
mainLoop hsSourceDirs cabal ldar = do
mainLoop :: [FilePath] -> FilePath -> D.GenericPackageDescription -> D.Library -> (FilePath, FilePath) -> IO ()
mainLoop hsSourceDirs cabal gpd lib ldar = do
ghcVer <- ghcVersion
_ <- rebuildCabal cmd
pkgArgs <- ghcPackageArgs isCabalDev ghcVer
rebuild <- mkRebuild ghcVer cabal cmd forceCabal ldar
let devArgs = pkgArgs ++ ["devel.hs"] ++ passThroughArgs
rebuild <- mkRebuild gpd ghcVer cabal opts ldar
forever $ do
recompDeps hsSourceDirs
list <- getFileList hsSourceDirs [cabal]
success <- rebuild
pkgArgs <- ghcPackageArgs opts ghcVer (D.packageDescription gpd) lib
let devArgs = pkgArgs ++ ["devel.hs"] ++ passThroughArgs
if not success
then putStrLn "Build failure, pausing..."
else do
removeLock
putStrLn $ "Starting development server: runghc " ++ L.unwords devArgs
putStrLn $ if verbose opts then "Starting development server: runghc " ++ L.unwords devArgs
else "Starting development server..."
(_,_,_,ph) <- createProcess $ proc "runghc" devArgs
watchTid <- forkIO . try_ $ do
watchForChanges hsSourceDirs [cabal] list
@ -123,34 +127,107 @@ devel isCabalDev forceCabal passThroughArgs = do
Ex.throwTo watchTid (userError "process finished")
watchForChanges hsSourceDirs [cabal] list
mkRebuild :: String -> FilePath -> String -> Bool -> (FilePath, FilePath) -> IO (IO Bool)
mkRebuild ghcVer cabalFile cabalCmd forceCabal (ldPath, arPath)
| forceCabal = return (rebuildCabal cabalCmd)
| GHC.cProjectVersion == ghcVer = do
bf <- getBuildFlags
{-
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
-}
configure :: FilePath -> D.GenericPackageDescription -> DevelOpts -> IO ()
configure cabal gpd opts
| isCabalDev opts = rawSystem (cabalCommand opts) args >> return ()
| otherwise = do
lbi <- D.configure (gpd, hookedBuildInfo) configFlags
D.writePersistBuildConfig "dist" lbi -- fixme we could keep this in memory instead of file
where
hookedBuildInfo = (Nothing, [])
configFlags | 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"
}
config = (DSS.defaultConfigFlags D.defaultProgramConfiguration)
{ DSS.configConfigurationsFlags =
[ (D.FlagName "devel", True) -- legaxy
, (D.FlagName "library-only", True)
]
, DSS.configProfLib = DSS.Flag False
, DSS.configUserInstall = DSS.Flag True
}
cabalArgs
| isCabalDev opts = map ("--cabal-install-arg=" ++) args
| otherwise = args
where
args =
[ "-fdevel" -- legacy
, "-flibrary-only"
] ++ wrapperArgs
wrapperArgs
| forceCabal opts = []
| otherwise =
[ "--with-compiler=yesod-ghc-wrapper"
, "--with-hc-pkg=ghc-pkg"
, "--with-ld=yesod-ld-wrapper"
, "--with-ar=yesod-ar-wrapper"
]
args :: [String]
args = "configure":cabalArgs ++ ["--disable-library-profiling" ]
removeFileIfExists :: FilePath -> IO ()
removeFileIfExists file = removeFile file `Ex.catch` handler
where
handler :: IOError -> IO ()
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)
| otherwise = do
return $ do
n1 <- cabalFile `isNewerThan` "dist/ghcargs.txt"
n2 <- cabalFile `isNewerThan` "dist/arargs.txt"
n3 <- cabalFile `isNewerThan` "dist/ldargs.txt"
if n1 || n2 || n3
then rebuildCabal cabalCmd
else rebuildGhc bf ldPath arPath
| otherwise = return $ do
putStrLn "WARNING: yesod is compiled with a different ghc version, falling back to cabal"
rebuildCabal cabalCmd
then rebuildCabal gpd opts
else do
bf <- getBuildFlags
rebuildGhc bf ldPath arPath
rebuildGhc :: [Located String] -> FilePath -> FilePath -> IO Bool
rebuildGhc bf ld ar = do
putStrLn "Rebuilding application... (GHC API)"
buildPackage bf ld ar
rebuildCabal :: String -> IO Bool
rebuildCabal cmd = do
putStrLn $ "Rebuilding application... (" ++ cmd ++ ")"
exit <- rawSystemFilter cmd ["build"]
return $ case exit of
rebuildCabal :: D.GenericPackageDescription -> DevelOpts -> IO Bool
rebuildCabal gpd opts
| isCabalDev opts = do
let cmd = cabalCommand opts
putStrLn $ "Rebuilding application... (" ++ cmd ++ ")"
exit <- (if verbose opts then rawSystem else rawSystemFilter) cmd ["build"]
return $ case exit of
ExitSuccess -> True
_ -> False
| otherwise = do
putStrLn $ "Rebuilding application... (Cabal library)"
lbi <- getPersistBuildConfig "dist" -- 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 a = (a >> return True) `Ex.catch` \(e::Ex.SomeException) -> do
putStrLn $ "Exception: " ++ show e
return False
try_ :: forall a. IO a -> IO ()
try_ x = (Ex.try x :: IO (Either Ex.SomeException a)) >> return ()
@ -179,7 +256,7 @@ checkDevelFile = do
e <- doesFileExist "devel.hs"
unless e $ failWith "file devel.hs not found"
checkCabalFile :: D.GenericPackageDescription -> IO [FilePath]
checkCabalFile :: D.GenericPackageDescription -> IO ([FilePath], D.Library)
checkCabalFile gpd = case D.condLibrary gpd of
Nothing -> failWith "incorrect cabal file, no library"
Just ct ->
@ -195,7 +272,7 @@ checkCabalFile gpd = case D.condLibrary gpd of
mapM_ putStrLn unlisted
when (D.fromString "Application" `notElem` D.exposedModules dLib) $
putStrLn "WARNING: no exposed module Application"
return hsSourceDirs
return (hsSourceDirs, dLib)
failWith :: String -> IO a
failWith msg = do
@ -223,22 +300,33 @@ ghcVersion = fmap getNumber $ readProcess "runghc" ["--numeric-version", "0"] []
where
getNumber = filter (\x -> isNumber x || x == '.')
ghcPackageArgs :: Bool -> String -> IO [String]
ghcPackageArgs isCabalDev ghcVer
| isCabalDev = do
r <- readProcess "cabal-dev" ["buildopts"] []
let opts = L.lines r
return $ "-hide-all-packages" : "-no-user-package-conf" : inplacePkg : cabaldevConf : pkgid opts : depPkgIds opts
| otherwise = return [inplacePkg]
ghcPackageArgs :: DevelOpts -> String -> D.PackageDescription -> D.Library -> IO [String]
ghcPackageArgs opts ghcVer cabal lib = do
lbi <- getPersistBuildConfig "dist"
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
pkgid opts = let (_,p) = head (selectOpts ["-package-name"] opts) in "-package-id" ++ p ++ "-inplace"
depPkgIds opts = map (uncurry (++)) (selectOpts ["-package-id"] opts)
inplacePkg = "-package-confdist/package.conf.inplace"
cabaldevConf = "-package-confcabal-dev/packages-" ++ ghcVer ++ ".conf"
selectOpts opts (x1:x2:xs)
| x1 `elem` opts = (x1,x2):selectOpts opts xs
| otherwise = selectOpts opts (x2:xs)
selectOpts _ _ = []
selfPkgArg lbi = pkgArg . D.inplacePackageId . D.package . D.localPkgDescr $ lbi
pkgArg (D.InstalledPackageId id) = "-package-id" ++ id
depArgs lbi cbi = map pkgArg (deps lbi cbi)
deps lbi cbi = let pkgInfo = D.inplaceInstalledPackageInfo "." "dist" cabal lib lbi cbi
in IPI.depends $ pkgInfo
errCbi = "No library ComponentBuildInfo"
cabalDevConf = "-package-confcabal-dev/packages-" ++ ghcVer ++ ".conf"
inplaceConf = "-package-confdist/package.conf.inplace"
getPersistBuildConfig :: FilePath ->IO D.LocalBuildInfo
getPersistBuildConfig path = fromMaybeErr errLbi =<< D.maybeGetPersistBuildConfig path
where
errLbi = "Could not read BuildInfo file: " ++ D.localBuildInfoFile "dist" ++
"\nMake sure that cabal-install has been compiled with the same GHC version as yesod."
fromMaybeErr :: String -> Maybe b -> IO b
fromMaybeErr err Nothing = failWith err
fromMaybeErr _ (Just x) = return x
lookupDevelLib :: D.CondTree D.ConfVar c a -> Maybe a
lookupDevelLib ct | found = Just (D.condTreeData ct)

View File

@ -19,6 +19,7 @@ module GhcBuild (getBuildFlags, buildPackage) where
import qualified Control.Exception as Ex
import System.Process (rawSystem)
import Control.Monad (when)
import Data.IORef
import qualified GHC
import DriverPhases ( Phase(..), isSourceFilename, anyHsc, startPhase, isHaskellSrcFilename )
@ -37,7 +38,15 @@ import Data.Maybe (fromMaybe)
import Panic (panic, ghcError)
import Data.List (partition, isPrefixOf)
import qualified DynFlags
import qualified StaticFlags
{-
This contains a huge hack:
GHC only accepts setting static flags once per process, however it has no way to
get the remaining options from the command line, without setting the static flags.
This code overwrites the IORef to disable the check. This will likely cause
problems if the flags are modified, but fortunately that's relatively uncommon.
-}
getBuildFlags :: IO [Located String]
getBuildFlags = do
argv0 <- fmap read $ readFile "dist/ghcargs.txt" -- generated by yesod-ghc-wrapper
@ -45,6 +54,7 @@ getBuildFlags = do
mbMinusB | null minusB_args = Nothing
| otherwise = Just (drop 2 (last minusB_args))
let argv1' = map (mkGeneralLocated "on the commandline") argv1
writeIORef StaticFlags.v_opt_C_ready False -- the huge hack
(argv2, staticFlagWarnings) <- GHC.parseStaticFlags argv1'
return argv2

View File

@ -4,7 +4,6 @@ module Types where
import Options
mkOptCabalDev name = option name (\o -> o
{ optionLongFlags = ["dev", "use-cabal-dev"]
, optionShortFlags = ['d']
@ -21,4 +20,19 @@ mkOptNoApi name = option name (\o -> o
, optionDescription = "do not use the GHC API to build, use `cabal build' instead"
})
mkOptApi name = option name (\o -> o
{ optionLongFlags = ["ghc-api"]
, optionShortFlags = ['a']
, optionType = optionTypeBool
, optionDefault = "false"
, optionDescription = "use the GHC API to build (faster, but experimental)"
})
mkOptVerbose name = option name (\o -> o
{ optionLongFlags = ["verbose"]
, optionShortFlags = ['v']
, optionType = optionTypeBool
, optionDefault = "false"
, optionDescription = "more verbose output"
})

View File

@ -10,7 +10,7 @@ import Options
import Types
import Build (touch)
import Devel (devel)
import Devel (devel, DevelOpts(..))
import System.IO (stdout, stderr, hPutStr, hPutStrLn)
import System.Exit (exitSuccess, exitFailure)
import Control.Monad.IO.Class (MonadIO, liftIO)
@ -18,10 +18,12 @@ import Control.Monad.IO.Class (MonadIO, liftIO)
defineOptions "NoOptions" (return ())
defineOptions "DevelOptions" $ do
mkOptNoApi "develOptNoApi"
mkOptApi "develOptApi"
-- mkOptNoApi "develOptNoApi" -- use this later when flag is enabled by default
defineOptions "MainOptions" $ do
mkOptCabalDev "optCabalDev"
mkOptVerbose "optVerbose"
type InitOptions = NoOptions
type ConfigureOptions = NoOptions
@ -50,10 +52,10 @@ cmdInit :: MainOptions -> InitOptions -> [String] -> IO ()
cmdInit _ _ _ = scaffold
cmdConfigure :: MainOptions -> ConfigureOptions -> [String] -> IO ()
cmdConfigure mopt opts args = exitWith =<< rawSystem (cabalCommand mopt) ("configure":args)
cmdConfigure mopt _ args = exitWith =<< rawSystem (cabalCommand mopt) ("configure":args)
cmdBuild :: MainOptions -> BuildOptions -> [String] -> IO ()
cmdBuild mopt opts args = do
cmdBuild mopt _ args = do
touch
exitWith =<< rawSystem (cabalCommand mopt) ("build":args)
@ -61,9 +63,11 @@ cmdTouch :: MainOptions -> TouchOptions -> [String] -> IO ()
cmdTouch _ _ _ = touch
cmdDevel :: MainOptions -> DevelOptions -> [String] -> IO ()
cmdDevel mopt opts args = devel (optCabalDev mopt) forceCabal args
cmdDevel mopt opts args = devel dopts args
where
forceCabal = develOptNoApi opts
dopts = DevelOpts (optCabalDev mopt) forceCabal (optVerbose mopt)
forceCabal = not (develOptApi opts)
-- forceCabal = develOptNoApi opts
cmdVersion :: MainOptions -> VersionOptions -> [String] -> IO ()
cmdVersion _ _ _ = putStrLn $ "yesod-core version: " ++ yesodVersion