cabal-build-programs (empty) → 0.1.0.0
raw patch · 5 files changed
+456/−0 lines, 5 filesdep +Cabaldep +basesetup-changed
Dependencies added: Cabal, base
Files
- LICENSE +30/−0
- README.org +9/−0
- Setup.hs +2/−0
- cabal-build-programs.cabal +39/−0
- src/Distribution/Simple/BuildPrograms.hs +376/−0
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright Aditya Siram (c) 2020++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++ * Redistributions of source code must retain the above copyright+ notice, this list of conditions and the following disclaimer.++ * Redistributions in binary form must reproduce the above+ copyright notice, this list of conditions and the following+ disclaimer in the documentation and/or other materials provided+ with the distribution.++ * Neither the name of Aditya Siram nor the names of other+ contributors may be used to endorse or promote products derived+ from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.org view
@@ -0,0 +1,9 @@+* cabal-build-programs+ A Cabal library that allows build time dependencies on non-Haskell+ executables. As of this writing there is no convenient Cabal field for+ non-Haskell programs that are sometimes useful in a polyglot build like+ 'cmake' or 'gprof'. This library solves that by providing a custom Cabal field+ "x-build-programs" which contains a list of such dependencies.++ See the [[https://hackage.haskell.org/package/cabal-build-programs/docs/Distribution-Simple-BuildPrograms.html][Haddock documentation]] for much more detail.+
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ cabal-build-programs.cabal view
@@ -0,0 +1,39 @@+cabal-version: 1.12++-- This file has been generated from package.yaml by hpack version 0.31.2.+--+-- see: https://github.com/sol/hpack+--+-- hash: fc55553ca764af941d36f1d2ab303878f4af5e82f36b24cf7d98008ec0dbbc4e++name: cabal-build-programs+version: 0.1.0.0+synopsis: Adds executable dependencies to the Cabal build+description: Please see the README on GitHub at <https://github.com/deech/cabal-build-programs#readme>+category: Distribution+homepage: https://github.com/deech/cabal-build-programs#readme+bug-reports: https://github.com/deech/cabal-build-programs/issues+author: Aditya Siram+maintainer: aditya.siram@gmail.com+copyright: Aditya Siram, 2019+license: BSD3+license-file: LICENSE+build-type: Simple+extra-source-files:+ README.org++source-repository head+ type: git+ location: https://github.com/deech/cabal-build-programs++library+ exposed-modules:+ Distribution.Simple.BuildPrograms+ other-modules:+ Paths_cabal_build_programs+ hs-source-dirs:+ src+ build-depends:+ Cabal >=2.2.0.0 && <4+ , base >=4.7 && <5+ default-language: Haskell2010
+ src/Distribution/Simple/BuildPrograms.hs view
@@ -0,0 +1,376 @@+{-# LANGUAGE ScopedTypeVariables #-}+module Distribution.Simple.BuildPrograms+ ( -- * Introduction+ --+ -- $introduction++ -- * Why?+ --+ -- $why++ -- * Getting Started+ --+ -- $gettingstarted++ -- * Limitations+ --+ -- $limitations++ -- * Stack & HPack Gotchas+ --+ -- $stackHPackGotchas++ buildProgramsCustomField+ , defaultMain+ , buildProgramsUserHooks+ , localBuildInfoWithBuildPrograms+ , componentBuildPrograms+ ) where++import Distribution.Simple(UserHooks(..),simpleUserHooks, defaultMainWithHooks)+import Distribution.Simple.Setup(BuildFlags(..), fromFlag,ReplFlags(..))+import Distribution.Types.LocalBuildInfo(LocalBuildInfo(..), neededTargetsInBuildOrder')+import Distribution.Types.PackageDescription(PackageDescription(..))+import Distribution.Types.Component(Component(..), componentBuildInfo, componentName)+import Distribution.Compat.Graph(nodeKey)+import Distribution.Types.BuildInfo(BuildInfo(customFieldsBI))+import Distribution.Types.TargetInfo(TargetInfo(..))+import Distribution.Simple.UserHooks(Args)+import Distribution.Simple.BuildTarget(readTargetInfos)+import Data.List(concatMap,unfoldr,nub,intersperse)+import Distribution.Verbosity(Verbosity)+import Control.Exception(catch,IOException)+import Control.Monad(foldM)+import Distribution.Pretty(prettyShow)+import Distribution.Simple.Program.Db(ProgramDb,requireProgram)+import Distribution.Simple.Program.Types(simpleProgram, Program(programName))+import Distribution.Simple.Utils(die')++{-|+The custom field to use in Cabal stanza to specify external programs.+In a ~.cabal~ file use:++@+x-build-programs: p1,p2,p3+@++whereas in the ~package.yaml~ of a <https://docs.haskellstack.org/en/stable/README/ Stack> or <https://hackage.haskell.org/package/hpack hpack> project you must use+(note that "p1,p2,p3" are quoted):++@+verbatim:+ x-build-programs: "p1,p2,p3"+@+-}+buildProgramsCustomField :: String+buildProgramsCustomField = "x-build-programs"++{-|+This function is the easiest way to use this library in your @Setup.hs@.++The default @Setup.hs@:++@+import Distribution.Simple+main = defaultMain+@++becomes:++@+import Distribution.Simple.BuildPrograms -- <- only change+main = defaultMain+@++-}+defaultMain :: IO ()+defaultMain = defaultMainWithHooks buildProgramsUserHooks++{-|+Directly access the 'UserHooks' that back 'defaultMain' when you need finer grained control, they are backed by 'simpleUserHooks':++@+import Distribution.Simple.BuildPrograms+import Distribution.Simple(defaultMainWithHooks)+main = defaultMainWithHooks buildProgramUserHooks+@+-}+buildProgramsUserHooks :: UserHooks+buildProgramsUserHooks = overlayUserHooks simpleUserHooks++{-|+If your @Setup.hs@ uses non-default or custom 'UserHooks' ,eg. 'autoconfUserHooks' this function will augment the 'buildHook' and 'replHook' to+first check for the external programs.++@+import Distribution.Simple.BuildPrograms+import Distribution.Simple(defaultMainWithHooks,autoconfUserHooks)+main = defaultMainWithHooks (overlayUserHooks autoconfUserHooks)+@+-}+overlayUserHooks :: UserHooks -> UserHooks+overlayUserHooks hooksToOverlay =+ let bh :: PackageDescription -> LocalBuildInfo -> UserHooks -> BuildFlags -> IO ()+ bh pd lbi uhs bfs = do+ newLbi <- localBuildInfoWithBuildPrograms (buildArgs bfs) (fromFlag (buildVerbosity bfs)) (hookedPrograms uhs) pd lbi+ case newLbi of+ Left err -> die' (fromFlag (buildVerbosity bfs)) err+ Right newLbi' -> (buildHook hooksToOverlay) pd newLbi' uhs bfs+ rh :: PackageDescription -> LocalBuildInfo -> UserHooks -> ReplFlags -> [String] -> IO ()+ rh pd lbi uhs rfs args = do+ newLbi <- localBuildInfoWithBuildPrograms args (fromFlag (replVerbosity rfs)) (hookedPrograms uhs) pd lbi+ case newLbi of+ Left err -> die' (fromFlag (replVerbosity rfs)) err+ Right newLbi' -> (replHook hooksToOverlay) pd newLbi' uhs rfs args+ in hooksToOverlay { buildHook = bh , replHook = rh }++splitProgramField :: String -> [String]+splitProgramField =+ let isComma t = t == ','+ isCommaOrSpace t = t == ' ' || isComma t+ split s =+ if null s+ then Nothing+ else Just (break isComma (dropWhile isCommaOrSpace s))+ in unfoldr split++programs :: [(String,String)] -> [String]+programs = nub . concatMap (splitProgramField . snd) . filter (\(field,_) -> field == buildProgramsCustomField)++customFields :: TargetInfo -> [(String,String)]+customFields = customFieldsBI . componentBuildInfo . targetComponent++{-|+Used internally it figures out which components to build, extracts the required+external programs into a lookup table. To make debugging easier I made it public+so you print the table.+-}+componentBuildPrograms :: Verbosity -> PackageDescription -> LocalBuildInfo -> Args -> IO [(Component, [String])]+componentBuildPrograms verb pd lbi args =+ let programFields :: TargetInfo -> (Component,[String])+ programFields t = (targetComponent t, programs (customFields t))+ in (map programFields . neededTargetsInBuildOrder' pd lbi . map nodeKey) <$> readTargetInfos verb pd lbi args++{-|+This is the lowest level function provided by this library for use in a highly+customized @Setup.hs@ scripts. Most of the other functions wrap it in some way.++'LocalBuildInfo' is a large datastructure that holds all the information+required to build a project, this function gathers up only the components that+need to be built ( or loaded in GHCi ), extracts the programs in the+"x-build-programs" fields, checks that they exist and adds them as+'ConfiguredProgram' to the 'ProgramDb' of the provided 'LocalBuildInfo' and+returns a new 'LocalBuildInfo' with the new 'ProgramDb'.++If any of the programs are cannot be found a formatted error message is returned.++The expectation is that it is called from a 'buildHook' or a 'replHook' at some+point before the components are built.+-}+localBuildInfoWithBuildPrograms ::+ Args -- ^ 'buildHook' ( extracted from 'BuildFlags') or 'replHook' arguments+ -> Verbosity+ -> [Program] -- ^ The 'hookedPrograms' from 'UserHooks', used to make sure we don't double check built-in build programs like 'c2hs' or 'ghc'+ -> PackageDescription+ -> LocalBuildInfo+ -> IO (Either String LocalBuildInfo) -- ^ Local build info with the external build programs configured or an error message+localBuildInfoWithBuildPrograms args verbosity hookedPs pd lbi =+ let pdFields = programs (customFieldsPD pd)+ combineAll :: [(Component, [String])] -> [(Maybe Component,[String])]+ combineAll cs = [(Nothing,nub pdFields)] ++ map (\(c,ps) -> (Just c,ps)) cs+ addOrLog :: [String] -> ProgramDb -> [String] -> IO ([String],ProgramDb)+ addOrLog errsSoFar programDb =+ foldM (\(errPs,db) p ->+ let logError = pure (errPs ++ [p], db)+ in+ if p `elem` errsSoFar+ then logError+ else+ if p `elem` (map programName hookedPs)+ then pure (errPs,db)+ else+ catch+ (do+ (_,newDb) <- requireProgram verbosity (simpleProgram p) db+ pure (errPs, newDb))+ (\(_ :: IOException) -> logError))+ ([],programDb)+ check :: [(Maybe Component,[String])] -> IO ([(Maybe Component, [String])],ProgramDb)+ check =+ foldM (\(compErrs,db) (comp,ps) -> do+ (errs,newDb) <- addOrLog (concatMap snd compErrs) db ps+ pure (compErrs ++ [(comp,errs)], newDb))+ ([],withPrograms lbi)+ compLabel :: Maybe Component -> String+ compLabel Nothing = "global"+ compLabel (Just c) = prettyShow (componentName c)+ commaSep :: [String] -> String+ commaSep = concat . intersperse ","+ printErr :: (Maybe Component, [String]) -> String+ printErr (comp,missing) = "- " ++ compLabel comp ++ ": " ++ commaSep missing+ in do+ allPrograms <- combineAll <$> componentBuildPrograms verbosity pd lbi args+ (compErrs, newDb) <- check allPrograms+ let errs = filter (not . null . snd) compErrs+ pure $+ if (not (null errs))+ then Left $ "\n" ++ (unlines ("These build programs are required but could not be found:"+ : ("- " ++ (commaSep . nub . concatMap snd) errs)+ : "Broken down by build component: "+ : map printErr errs))+ else Right $ lbi { withPrograms = newDb }++-- $introduction+-- This is a Cabal library that lets you annotate your+-- @.cabal@ (or @package.yaml@) file with external programs that must be+-- available at build time. It does this using a custom Cabal field,+-- @x-build-programs@ which takes a list of executables that are checked+-- whenever the project is built or when starting GHCi (unfortunately the+-- latter does not work with Stack, see "Stack & HPack Gotchas" below). It can+-- also be specified per build component (library, executables, benchmarks, test+-- suite etc.) so that, for instance, a library does not have to depend on an+-- external program used just for benchmarking (eg. 'gprof').++-- $why+-- This library exists because currently there is no field+-- in Cabal that checks for the existence of non-Haskell programs (eg. '7z.exe'+-- or 'cmake') at build time. The closest ,+-- <https://www.haskell.org/cabal/users-guide/developing-packages.html#pkg-field-build-tool-depends 'build-tool-depends'>+-- only allows Haskell-buildable exectuables like 'c2hs'.++-- $gettingstarted+-- First off you will need to set up a custom build in your @.cabal@:+--+-- @+-- build-type: Custom+-- custom-setup+-- setup-depends:+-- Cabal >=2.2.0.0 && <4+-- , base >=4.7 && <5+-- , cabal-build-programs+-- @+--+-- or @package.yaml@ file:+--+-- @+-- build-type: Custom+-- custom-setup:+-- dependencies:+-- - base >= 4.7 && < 5+-- - Cabal >= 2.2.0.0 && < 4+-- - cabal-build-programs+-- @+--+--+-- Then add the external program dependencies, as in this example @package.yaml@:+--+-- @+-- verbatim:+-- x-build-programs: "cmake"+--+-- library:+-- ...+-- verbatim:+-- x-build-programs: "llmv-config"+-- when:+-- - condition: os(windows)+-- then:+-- verbatim:+-- x-build-programs: "7z.exe"+-- else:+-- verbatim:+-- x-build-programs: "zip"+--+-- benchmarks:+-- my-benchmarks:+-- ...+-- verbatim:+-- x-build-programs: "gprof,valgrind"+-- @+--+--+-- A project that uses the snippet above always depends on @cmake@ no matter+-- which artifact is being built but only the library needs @llvm-config@. On+-- Windows the library needs @7z.exe@ for extracting archives, otherwise @zip@+-- and the benchmarks always need the GNU profiling @gprof@ tool and @valgrind@.+--+-- Now we need a custom @Setup.hs@ that can use this library to check those+-- dependencies.+--+-- The easiest way is to use the provided 'defaultMain'. For more advanced use+-- check the docs for 'buildProgramsUserHooks' and+-- 'localBuildInfoWithBuildPrograms'.+--+--++-- $stackHPackGotchas+-- <https://docs.haskellstack.org/en/stable/README/ Stack> and <https://github.com/sol/hpack Hpack> have+-- are a few gotchas to keep in mind and since Stack uses HPack it inherits all the issues as well.+--+-- * Stack seems to completely ignore the 'preRepl' and 'replHook' so+-- when using Stack external programs are not checked when using @stack+-- ghci@ or @stack repl@, @stack build@ seems to work fine.+--+-- * HPack requires that all custom fields be in a "verbatim:" block+-- so the first snippet below is rejected but the second is not:+--+-- @+-- name: my-awesome-program+-- ...+-- x-build-programs: "cmake,gprof,someOtherDep"+-- ...+-- @+--+-- @+-- name: my-awesome-program+-- ...+-- verbatim:+-- x-build-programs: "cmake,gprof,someOtherDep"+-- ...+-- @+--+-- * Unlike Cabal, HPack will not accumulate across duplicate custom fields+-- and picks the last one it encounters but only at the same level. For+-- example in the first snippet (a) will be ignored by HPack and you will+-- only see (b) in your @.cabal@ file. But in the second snippet since (a)+-- is inside a conditional it does make it in. To be fair HPack+-- will warn in the first case but it's pretty easy to miss.+--+-- @+-- executables:+-- my-awesome-executable+-- main: Main.hs+-- verbatim:+-- x-build-programs: "cmake,prof" (a)+-- ghc-options:+-- - ...+-- verbatim:+-- x-build-programs: "someOtherDep" (b)+-- @+--+-- @+-- executables:+-- my-awesome-executable+-- main: Main.hs+-- when:+-- - condition: os(linux)+-- verbatim:+-- x-build-programs: "cmake,prof" (a)+-- ghc-options:+-- - ...+-- verbatim:+-- x-build-programs: "someOtherDep" (b)+-- @+--++-- $limitations+-- The biggest limitations of this library is it cannot do any kind of version+-- checking of external programs. For example, it is currently not possible to+-- depend on eg. @cmake > 3.0.0@. It simply looks around in the environment for+-- the first executable that matches the name.+--+-- The 'Program' datastructure provided by Cabal is a+-- lot richer providing a way of extracting versions and doing some post+-- configuration, see the builtin+-- <https://hackage.haskell.org/package/Cabal-3.0.0.0/docs/Distribution-Simple-Program.html#v:tarProgram tar> as+-- an example. But it is also more complicated use.