pier-0.3.0.0: src/Pier/Build/Components.hs
{-# LANGUAGE DeriveAnyClass #-}
module Pier.Build.Components
( buildPackageRules
, askBuiltLibrary
, askMaybeBuiltLibrary
, askBuiltExecutables
, askBuiltExecutable
, askBuiltTestSuite
, askBuiltTestSuites
, BuiltBinary(..)
)
where
import Control.Applicative (liftA2)
import Control.Monad (filterM, (>=>))
import Data.List (find)
import Data.Maybe (fromMaybe)
import Development.Shake
import Development.Shake.Classes
import Development.Shake.FilePath hiding (exe)
import Distribution.Package
import Distribution.PackageDescription
import Distribution.System (buildOS, OS(..))
import Distribution.Text
import GHC.Generics hiding (packageName)
import qualified Data.Map as Map
import qualified Data.Set as Set
import qualified Distribution.InstalledPackageInfo as IP
import Pier.Build.Config
import Pier.Build.ConfiguredPackage
import Pier.Build.Executable
import Pier.Build.CFlags
import Pier.Build.Stackage
import Pier.Build.TargetInfo
import Pier.Core.Artifact
import Pier.Core.Persistent
buildPackageRules :: Rules ()
buildPackageRules = do
addPersistent buildLibrary
addPersistent getBuiltinLib
addPersistent buildExecutables
addPersistent buildExecutable
addPersistent buildTestSuites
addPersistent buildTestSuite
newtype BuiltLibraryQ = BuiltLibraryQ PackageName
deriving (Typeable, Eq, Generic, Hashable, Binary, NFData)
type instance RuleResult BuiltLibraryQ = Maybe BuiltLibrary
instance Show BuiltLibraryQ where
show (BuiltLibraryQ p) = "Library " ++ display p
-- ghc --package-db .../text-1234.pkg/db --package text-1234
data BuiltLibrary = BuiltLibrary
{ builtPackageId :: PackageIdentifier
, builtPackageTrans :: TransitiveDeps
}
deriving (Show,Typeable,Eq,Hashable,Binary,NFData,Generic)
askBuiltLibraries :: [PackageName] -> Action [BuiltLibrary]
askBuiltLibraries = flip forP askBuiltLibrary
askMaybeBuiltLibrary :: PackageName -> Action (Maybe BuiltLibrary)
askMaybeBuiltLibrary pkg = askPersistent (BuiltLibraryQ pkg)
askBuiltLibrary :: PackageName -> Action BuiltLibrary
askBuiltLibrary pkg = askMaybeBuiltLibrary pkg >>= helper
where
helper Nothing = error $ "buildFromDesc: " ++ display pkg
++ " does not have a buildable library"
helper (Just lib) = return lib
data BuiltDeps = BuiltDeps [PackageIdentifier] TransitiveDeps
deriving Show
askBuiltDeps
:: [PackageName]
-> Action BuiltDeps
askBuiltDeps pkgs = do
deps <- askBuiltLibraries pkgs
return $ BuiltDeps (dedup $ map builtPackageId deps)
(foldMap builtPackageTrans deps)
where
dedup = Set.toList . Set.fromList
buildLibrary :: BuiltLibraryQ -> Action (Maybe BuiltLibrary)
buildLibrary (BuiltLibraryQ pkg) =
getConfiguredPackage pkg >>= \case
Left p -> Just . BuiltLibrary p <$> askBuiltinLibrary
(packageIdToUnitId p)
Right confd
| Just lib <- library (confdDesc confd)
, let bi = libBuildInfo lib
, buildable bi -> Just <$> do
deps <- askBuiltDeps $ targetDepNames bi
buildLibraryFromDesc deps confd lib
| otherwise -> return Nothing
where
packageIdToUnitId :: PackageId -> UnitId
packageIdToUnitId = mkUnitId . display
getBuiltinLib :: BuiltinLibraryR -> Action TransitiveDeps
getBuiltinLib (BuiltinLibraryR p) = do
conf <- askConfig
let ghc = configGhc conf
result <- runCommandStdout
$ ghcPkgProg ghc
["describe" , display p]
info <- case IP.parseInstalledPackageInfo result of
IP.ParseFailed err -> error (show err)
IP.ParseOk _ info -> return info
deps <- mapM askBuiltinLibrary $ IP.depends info
let paths f = Set.fromList . map (parseGlobalPackagePath ghc)
. f $ info
return $ mconcat deps <> TransitiveDeps
{ transitiveDBs = Set.empty
-- Don't bother tracking compile-time files for built-in
-- libraries, since they're already provided implicitly
-- by `ghcProg`.
, transitiveLibFiles = Set.empty
, transitiveIncludeDirs = paths IP.includeDirs
-- Make dynamic libraries available at runtime,
-- falling back to the regular dir if it's not set
-- (usually these will be the same).
, transitiveDataFiles = paths IP.libraryDirs
<> paths IP.libraryDynDirs
}
askBuiltinLibrary :: UnitId -> Action TransitiveDeps
askBuiltinLibrary = askPersistent . BuiltinLibraryR
newtype BuiltinLibraryR = BuiltinLibraryR UnitId
deriving (Typeable, Eq, Generic, Hashable, Binary, NFData)
type instance RuleResult BuiltinLibraryR = TransitiveDeps
instance Show BuiltinLibraryR where
show (BuiltinLibraryR p) = "Library " ++ display p ++ " (built-in)"
buildLibraryFromDesc
:: BuiltDeps
-> ConfiguredPackage
-> Library
-> Action BuiltLibrary
buildLibraryFromDesc deps@(BuiltDeps _ transDeps) confd lib = do
let pkg = package $ confdDesc confd
conf <- askConfig
let ghc = configGhc conf
let lbi = libBuildInfo lib
tinfo <- getTargetInfo confd lbi (TargetLibrary $ exposedModules lib)
transDeps ghc
maybeLib <- if null $ exposedModules lib
then return Nothing
else do
let hiDir = "hi"
let oDir = "o"
let libHSName = "HS" ++ display (packageName pkg)
let dynLibFile = "lib" ++ libHSName
++ "-ghc" ++ display (ghcVersion $ plan conf)
<.> dynExt
(hiDir', dynLib) <- runCommand
(liftA2 (,) (output hiDir) (output dynLibFile))
$ message (display pkg ++ ": building library")
<> ghcCommand ghc deps confd tinfo
(ghcOptions conf ++
[ "-this-unit-id", display pkg
, "-hidir", hiDir
, "-hisuf", "dyn_hi"
, "-osuf", "dyn_o"
, "-odir", oDir
, "-shared", "-dynamic"
, "-o", dynLibFile
])
return $ Just (libHSName, lib, dynLib, hiDir')
(pkgDb, libFiles) <- registerPackage ghc pkg lbi
(targetCFlags tinfo) maybeLib
deps
let linkerData = maybe Set.empty (\(_,_,dyn,_) -> Set.singleton dyn)
maybeLib
transInstallIncludes <- collectInstallIncludes (confdSourceDir confd) lbi
return $ BuiltLibrary pkg
$ transDeps <> TransitiveDeps
{ transitiveDBs = Set.singleton pkgDb
, transitiveLibFiles = Set.singleton libFiles
, transitiveIncludeDirs =
maybe Set.empty Set.singleton transInstallIncludes
, transitiveDataFiles = linkerData
-- TODO: just the lib
<> Set.singleton libFiles
}
-- TODO: double-check no two executables with the same name
newtype BuiltExecutablesQ = BuiltExecutablesQ PackageName
deriving (Typeable, Eq, Generic, Hashable, Binary, NFData)
type instance RuleResult BuiltExecutablesQ = [BuiltBinary]
instance Show BuiltExecutablesQ where
show (BuiltExecutablesQ p) = "Executables from " ++ display p
askBuiltExecutables :: PackageName -> Action [BuiltBinary]
askBuiltExecutables = askPersistent . BuiltExecutablesQ
data BuiltTestSuiteQ = BuiltTestSuiteQ PackageName String
deriving (Typeable, Eq, Generic, Hashable, Binary, NFData)
type instance RuleResult BuiltTestSuiteQ = BuiltBinary
instance Show BuiltTestSuiteQ where
show (BuiltTestSuiteQ p s) = "TestSuite " ++ s ++ " from " ++ display p
askBuiltTestSuite :: PackageName -> String -> Action BuiltBinary
askBuiltTestSuite p e = askPersistent $ BuiltTestSuiteQ p e
buildExecutables :: BuiltExecutablesQ -> Action [BuiltBinary]
buildExecutables (BuiltExecutablesQ p) = getConfiguredPackage p >>= \case
Left _ -> return []
Right confd ->
mapM (buildBinaryFromPkg confd . exeSpec)
. filter (buildable . buildInfo)
$ executables (confdDesc confd)
-- TODO: error if not buildable?
buildExecutable :: BuiltExecutableQ -> Action BuiltBinary
buildExecutable (BuiltExecutableQ p e) = getConfiguredPackage p >>= \case
Left pid -> error $ "Built-in package " ++ display pid
++ " has no executables"
Right confd
| Just exe <- find ((== e) . display . exeName) (executables $ confdDesc confd)
-> buildBinaryFromPkg confd (exeSpec exe)
| otherwise -> error $ "Package " ++ display (packageId confd)
++ " has no executable named " ++ e
data BinarySpec = BinarySpec
{ binaryTypeName :: String
, binaryName :: String
, binaryPath :: FilePath
, binaryBuildInfo :: BuildInfo
}
exeSpec :: Executable -> BinarySpec
exeSpec e = BinarySpec
{ binaryTypeName = "executable"
, binaryName = display $ exeName e
, binaryPath = modulePath e
, binaryBuildInfo = buildInfo e
}
testSpec :: TestSuite -> Action BinarySpec
testSpec t@TestSuite { testInterface = TestSuiteExeV10 _ path }
= return BinarySpec
{ binaryTypeName = "test-suite"
, binaryName = display $ testName t
, binaryPath = path
, binaryBuildInfo = testBuildInfo t
}
testSpec t = fail $ "Unknown test type " ++ show (testInterface t)
++ " for test " ++ display (testName t)
buildBinaryFromPkg
:: ConfiguredPackage
-> BinarySpec
-> Action BuiltBinary
buildBinaryFromPkg confd bin = do
let desc = confdDesc confd
deps@(BuiltDeps _ transDeps)
<- askBuiltDeps $ exeDepNames desc (binaryBuildInfo bin)
conf <- askConfig
let ghc = configGhc conf
let out = "bin" </> binaryName bin
tinfo <- getTargetInfo confd (binaryBuildInfo bin) (TargetBinary $ binaryPath bin)
transDeps ghc
result <- runCommandOutput out
$ message (display (package desc) ++ ": building "
++ binaryTypeName bin ++ " " ++ binaryName bin)
<> ghcCommand ghc deps confd tinfo
(ghcOptions conf ++
[ "-o", out
, "-hidir", "hi"
, "-odir", "o"
, "-dynamic"
, "-threaded"
])
return BuiltBinary
{ builtBinary = result
, builtBinaryDataFiles = foldr Set.insert (transitiveDataFiles transDeps)
(confdDataFiles confd)
}
newtype BuiltTestSuitesQ = BuiltTestSuitesQ PackageName
deriving (Typeable, Eq, Generic, Hashable, Binary, NFData)
type instance RuleResult BuiltTestSuitesQ = [BuiltBinary]
instance Show BuiltTestSuitesQ where
show (BuiltTestSuitesQ p) = "Test suites from " ++ display p
askBuiltTestSuites :: PackageName -> Action [BuiltBinary]
askBuiltTestSuites = askPersistent . BuiltTestSuitesQ
buildTestSuites :: BuiltTestSuitesQ -> Action [BuiltBinary]
buildTestSuites (BuiltTestSuitesQ p) = getConfiguredPackage p >>= \case
Left _ -> return []
Right confd ->
mapM (testSpec >=> buildBinaryFromPkg confd)
. filter (buildable . testBuildInfo)
$ testSuites (confdDesc confd)
-- TODO: error if not buildable?
buildTestSuite :: BuiltTestSuiteQ -> Action BuiltBinary
buildTestSuite (BuiltTestSuiteQ p s) = getConfiguredPackage p >>= \case
Left pid -> error $ "Built-in package " ++ display pid
++ " has no test suites"
Right confd
| Just suite <-
find ((== s) . display . testName) (testSuites $ confdDesc confd)
-> testSpec suite >>= buildBinaryFromPkg confd
| otherwise -> error $ "Package " ++ display (packageId confd)
++ " has no test suite named " ++ s
ghcCommand
:: InstalledGhc
-> BuiltDeps
-> ConfiguredPackage
-> TargetInfo
-> [String]
-> Command
ghcCommand ghc (BuiltDeps depPkgs transDeps) confd tinfo args
= inputs (transitiveDBs transDeps)
<> inputs (transitiveLibFiles transDeps)
<> inputList (targetSourceInputs tinfo ++ targetOtherInputs tinfo)
-- Embed extra-source-files two ways: as regular inputs, and shadowed
-- directly into the working directory.
-- They're needed as regular inputs so that, if they're headers, they
-- stay next to c-sources (which the C include system expects).
-- They're needed directly in the working directory to be available to
-- template haskell splices.
<> inputList (map pkgFile $ confdExtraSrcFiles confd)
<> foldMap (\f -> shadow (pkgFile f) f) (confdExtraSrcFiles confd)
<> ghcProg ghc (allArgs ++ map pathIn (targetSourceInputs tinfo))
where
cflags = targetCFlags tinfo
pkgFile = (confdSourceDir confd />)
allArgs =
-- Rely on GHC for module ordering and hs-boot files:
[ "--make"
, "-v0"
, "-fPIC"
, "-i"
]
-- Necessary for boot files:
++ map (("-i" ++) . pathIn) (targetSourceDirs tinfo)
++
concatMap (\p -> ["-package-db", pathIn p])
(Set.toList $ transitiveDBs transDeps)
++
concat [["-package", display d] | d <- depPkgs]
-- Include files which are sources
++ map (("-I" ++) . pathIn . pkgFile) (targetIncludeDirs tinfo)
-- Include files which are listed as extra-src-files, and thus shadowed directly into
-- the working dir:
++ map ("-I" ++) (targetIncludeDirs tinfo)
++ targetOptions tinfo
++ map ("-optP" ++) (cppFlags cflags)
++ ["-optc" ++ opt | opt <- ccFlags cflags]
++ ["-l" ++ libDep | libDep <- linkLibs cflags]
++ ["-optl" ++ f | f <- linkFlags cflags]
++ concat [["-framework", f] | f <- macFrameworks cflags]
-- TODO: configurable
++ ["-O0"]
-- TODO: just for local builds
++ ["-w"]
++ args
registerPackage
:: InstalledGhc
-> PackageIdentifier
-> BuildInfo
-> CFlags
-> Maybe ( String -- Library name for linking
, Library
, Artifact -- dyn lib archive
, Artifact -- hi
)
-> BuiltDeps
-> Action (Artifact, Artifact)
registerPackage ghc pkg bi cflags maybeLib (BuiltDeps depPkgs transDeps)
= do
let pre = "files"
let depsByName = Map.fromList [(packageName p, p) | p <- depPkgs]
let (collectLibInputs, libDesc) = case maybeLib of
Nothing -> (createDirectoryA pre, [])
Just (libHSName, lib, dynLibA, hi) ->
( shadow dynLibA (pre </> takeFileName (pathIn dynLibA))
<> shadow hi (pre </> "hi")
, [ "hs-libraries: " ++ libHSName
, "library-dirs: ${pkgroot}" </> pre
, "dynamic-library-dirs: ${pkgroot}" </> pre
, "import-dirs: ${pkgroot}" </> pre </> "hi"
, "exposed-modules: " ++
unwords (map display (exposedModules lib)
++ map (renderReexport depsByName)
(reexportedModules lib))
, "hidden-modules: " ++ unwords (map display $ otherModules bi)
]
)
spec <- writeArtifact "spec" $ unlines $
[ "name: " ++ display (packageName pkg)
, "version: " ++ display (packageVersion pkg)
, "id: " ++ display pkg
, "key: " ++ display pkg
, "extra-libraries: " ++ unwords (linkLibs cflags)
-- TODO: this list should be string-separated, and make sure
-- to quote flags that contain strings (e.g. "-Wl,-E" from hslua).
-- , "ld-options: " ++ unwords (linkFlags cflags)
, "depends: " ++ unwords (map display depPkgs)
]
++ [ "frameworks: " ++ unwords (macFrameworks cflags)
| not (null $ macFrameworks cflags)
]
++ libDesc
let db = display pkg
runCommand (liftA2 (,) (output db) (output pre))
$ collectLibInputs
<> ghcPkgProg ghc ["init", db]
<> ghcPkgProg ghc
(["-v0"]
++ [ "--package-db=" ++ pathIn f
| f <- Set.toList $ transitiveDBs transDeps
]
++ ["--package-db", db, "register",
pathIn spec])
<> input spec
<> inputs (transitiveDBs transDeps)
dynExt :: String
dynExt = case buildOS of
OSX -> "dylib"
_ -> "so"
renderReexport ::
Map.Map PackageName PackageIdentifier -> ModuleReexport -> String
renderReexport deps re = display (moduleReexportName re) ++ " from "
++ maybe "" (\pkg -> display (originalPkg pkg) ++ ":")
(moduleReexportOriginalPackage re)
++ display (moduleReexportOriginalName re)
where
originalPkg p =
fromMaybe (error $ "Unknown package name " ++ display p
++ " for module reexport " ++ display re)
$ Map.lookup p deps
collectInstallIncludes :: Artifact -> BuildInfo -> Action (Maybe Artifact)
collectInstallIncludes dir bi
| null (installIncludes bi) = pure Nothing
| otherwise = fmap Just (mapM locateHeader (installIncludes bi)
>>= groupFiles dir)
where
-- | Returns the actual location of that header (potentially in some includeDir)
-- paired with the original name of that header without the dir.
locateHeader :: FilePath -> Action (FilePath, FilePath)
locateHeader f = do
let candidates = map (\d -> (d, dir /> d </> f)) ("" : includeDirs bi)
existing <- filterM (doesArtifactExist . snd) candidates
case existing of
(d, _):_ -> return (d </> f, f)
_ -> error $ "Couldn't locate install-include " ++ show f