Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 755e002

Browse files
committedFeb 26, 2025·
Add pre and post build hooks
Run a program (named "preBuildHook") before doing a package build and another program (named "postBuildHook") after the package is built. The exit code from the pre-build hook is passed to the post-build hook. The commit includes documentation for the hooks and the security safeguards implemented to avoid the running of malicious hook files.
1 parent 61413b6 commit 755e002

File tree

18 files changed

+318
-3
lines changed

18 files changed

+318
-3
lines changed
 

‎cabal-install/cabal-install.cabal

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ library
141141
Distribution.Client.GlobalFlags
142142
Distribution.Client.Haddock
143143
Distribution.Client.HashValue
144+
Distribution.Client.HookAccept
144145
Distribution.Client.HttpUtils
145146
Distribution.Client.IndexUtils
146147
Distribution.Client.IndexUtils.ActiveRepos

‎cabal-install/src/Distribution/Client/CmdFreeze.hs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ freezeAction flags@NixStyleFlags{..} extraArgs globalFlags = do
142142
(_, elaboratedPlan, _, totalIndexState, activeRepos) <-
143143
rebuildInstallPlan
144144
verbosity
145+
mempty
145146
distDirLayout
146147
cabalDirLayout
147148
projectConfig

‎cabal-install/src/Distribution/Client/CmdTarget.hs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ targetAction flags@NixStyleFlags{..} ts globalFlags = do
160160
(_, elaboratedPlan, _, _, _) <-
161161
rebuildInstallPlan
162162
verbosity
163+
mempty
163164
distDirLayout
164165
cabalDirLayout
165166
projectConfig

‎cabal-install/src/Distribution/Client/Errors.hs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,8 @@ data CabalInstallException
186186
| MissingPackageList Repo.RemoteRepo
187187
| CmdPathAcceptsNoTargets
188188
| CmdPathCommandDoesn'tSupportDryRun
189+
| HookAcceptUnknown FilePath FilePath String
190+
| HookAcceptHashMismatch FilePath FilePath String String
189191
deriving (Show)
190192

191193
exceptionCodeCabalInstall :: CabalInstallException -> Int
@@ -338,6 +340,8 @@ exceptionCodeCabalInstall e = case e of
338340
MissingPackageList{} -> 7160
339341
CmdPathAcceptsNoTargets{} -> 7161
340342
CmdPathCommandDoesn'tSupportDryRun -> 7163
343+
HookAcceptUnknown{} -> 7164
344+
HookAcceptHashMismatch{} -> 7165
341345

342346
exceptionMessageCabalInstall :: CabalInstallException -> String
343347
exceptionMessageCabalInstall e = case e of
@@ -860,6 +864,36 @@ exceptionMessageCabalInstall e = case e of
860864
"The 'path' command accepts no target arguments."
861865
CmdPathCommandDoesn'tSupportDryRun ->
862866
"The 'path' command doesn't support the flag '--dry-run'."
867+
HookAcceptUnknown hsPath fpath hash ->
868+
concat
869+
[ "The following file does not appear in the hooks-security file.\n"
870+
, " hook file : "
871+
, fpath
872+
, "\n"
873+
, " file hash : "
874+
, hash
875+
, "\n"
876+
, "After checking the contents of that file, it should be added to the\n"
877+
, "hooks-security file with either AcceptAlways or better yet an AcceptHash.\n"
878+
, "The hooks-security file is (probably) located at: "
879+
, hsPath
880+
]
881+
HookAcceptHashMismatch hsPath fpath expected actual ->
882+
concat
883+
[ "\nHook file hash mismatch for:\n"
884+
, " hook file : "
885+
, fpath
886+
, "\n"
887+
, " expected hash: "
888+
, expected
889+
, "\n"
890+
, " actual hash : "
891+
, actual
892+
, "\n"
893+
, "The hook file should be inspected and if deemed ok, the hooks-security file updated.\n"
894+
, "The hooks-security file is (probably) located at: "
895+
, hsPath
896+
]
863897

864898
instance Exception (VerboseException CabalInstallException) where
865899
displayException :: VerboseException CabalInstallException -> [Char]

‎cabal-install/src/Distribution/Client/HashValue.hs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
module Distribution.Client.HashValue
55
( HashValue
66
, hashValue
7+
, hashValueFromHex
78
, truncateHash
89
, showHashValue
910
, readFileHashValue
@@ -51,6 +52,11 @@ instance Structured HashValue
5152
hashValue :: LBS.ByteString -> HashValue
5253
hashValue = HashValue . SHA256.hashlazy
5354

55+
-- From a base16 encoded Bytestring to a HashValue with `Base16`'s
56+
-- error passing through.
57+
hashValueFromHex :: BS.ByteString -> Either String HashValue
58+
hashValueFromHex bs = HashValue <$> Base16.decode bs
59+
5460
showHashValue :: HashValue -> String
5561
showHashValue (HashValue digest) = BS.unpack (Base16.encode digest)
5662

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
{-# LANGUAGE DeriveGeneric #-}
2+
{-# LANGUAGE OverloadedStrings #-}
3+
4+
module Distribution.Client.HookAccept
5+
( HookAccept (..)
6+
, assertHookHash
7+
, loadHookHasheshMap
8+
, parseHooks
9+
) where
10+
11+
import Distribution.Client.Compat.Prelude
12+
13+
import Data.ByteString.Char8 (ByteString)
14+
import qualified Data.ByteString.Char8 as BS
15+
16+
import qualified Data.Map.Strict as Map
17+
18+
import Distribution.Client.Config (getConfigFilePath)
19+
import Distribution.Client.Errors (CabalInstallException (..))
20+
import Distribution.Client.HashValue (HashValue, hashValueFromHex, readFileHashValue, showHashValue)
21+
import Distribution.Simple.Setup (Flag (..))
22+
import Distribution.Simple.Utils (dieWithException)
23+
import Distribution.Verbosity (normal)
24+
25+
import System.FilePath (takeDirectory, (</>))
26+
27+
data HookAccept
28+
= AcceptAlways
29+
| AcceptHash HashValue
30+
deriving (Eq, Show, Generic)
31+
32+
instance Monoid HookAccept where
33+
mempty = AcceptAlways -- Should never be needed.
34+
mappend = (<>)
35+
36+
instance Semigroup HookAccept where
37+
AcceptAlways <> AcceptAlways = AcceptAlways
38+
AcceptAlways <> AcceptHash h = AcceptHash h
39+
AcceptHash h <> AcceptAlways = AcceptHash h
40+
AcceptHash h <> _ = AcceptHash h
41+
42+
instance Binary HookAccept
43+
instance Structured HookAccept
44+
45+
assertHookHash :: Map FilePath HookAccept -> FilePath -> IO ()
46+
assertHookHash m fpath = do
47+
actualHash <- readFileHashValue fpath
48+
hsPath <- getHooksSecurityFilePath NoFlag
49+
case Map.lookup fpath m of
50+
Nothing ->
51+
dieWithException normal $
52+
HookAcceptUnknown hsPath fpath (showHashValue actualHash)
53+
Just AcceptAlways -> pure ()
54+
Just (AcceptHash expectedHash) ->
55+
when (actualHash /= expectedHash) $
56+
dieWithException normal $
57+
HookAcceptHashMismatch
58+
hsPath
59+
fpath
60+
(showHashValue expectedHash)
61+
(showHashValue actualHash)
62+
63+
getHooksSecurityFilePath :: Flag FilePath -> IO FilePath
64+
getHooksSecurityFilePath configFileFlag = do
65+
hfpath <- getConfigFilePath configFileFlag
66+
pure $ takeDirectory hfpath </> "hooks-security"
67+
68+
loadHookHasheshMap :: Flag FilePath -> IO (Map FilePath HookAccept)
69+
loadHookHasheshMap configFileFlag = do
70+
hookFilePath <- getHooksSecurityFilePath configFileFlag
71+
handleNotExists $ fmap parseHooks (BS.readFile hookFilePath)
72+
where
73+
handleNotExists :: IO (Map FilePath HookAccept) -> IO (Map FilePath HookAccept)
74+
handleNotExists action = catchIO action $ \_ -> return mempty
75+
76+
parseHooks :: ByteString -> Map FilePath HookAccept
77+
parseHooks = Map.fromList . map parse . cleanUp . BS.lines
78+
where
79+
cleanUp :: [ByteString] -> [ByteString]
80+
cleanUp = filter (not . BS.null) . map rmComments
81+
82+
rmComments :: ByteString -> ByteString
83+
rmComments = fst . BS.breakSubstring "--"
84+
85+
parse :: ByteString -> (FilePath, HookAccept)
86+
parse bs =
87+
case BS.words bs of
88+
[fp, "AcceptAlways"] -> (BS.unpack fp, AcceptAlways)
89+
[fp, "AcceptHash"] -> buildAcceptHash fp "00"
90+
[fp, "AcceptHash", h] -> buildAcceptHash fp h
91+
_ -> error $ "Not able to parse:" ++ show bs
92+
where
93+
buildAcceptHash :: ByteString -> ByteString -> (FilePath, HookAccept)
94+
buildAcceptHash fp h =
95+
case hashValueFromHex h of
96+
Left err -> error $ "Distribution.Client.HookAccept.parse :" ++ err
97+
Right hv -> (BS.unpack fp, AcceptHash hv)

‎cabal-install/src/Distribution/Client/ProjectBuilding/UnpackedPackage.hs

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ module Distribution.Client.ProjectBuilding.UnpackedPackage
3030
import Distribution.Client.Compat.Prelude
3131
import Prelude ()
3232

33+
import Distribution.Client.HookAccept (assertHookHash)
3334
import Distribution.Client.PackageHash (renderPackageHashInputs)
3435
import Distribution.Client.ProjectBuilding.Types
3536
import Distribution.Client.ProjectConfig
@@ -105,7 +106,7 @@ import qualified Data.ByteString.Lazy.Char8 as LBS.Char8
105106
import qualified Data.List.NonEmpty as NE
106107

107108
import Control.Exception (ErrorCall, Handler (..), SomeAsyncException, assert, catches, onException)
108-
import System.Directory (canonicalizePath, createDirectoryIfMissing, doesDirectoryExist, doesFileExist, removeFile)
109+
import System.Directory (canonicalizePath, createDirectoryIfMissing, doesDirectoryExist, doesFileExist, getCurrentDirectory, removeFile)
109110
import System.FilePath (dropDrive, normalise, takeDirectory, (<.>), (</>))
110111
import System.IO (Handle, IOMode (AppendMode), withFile)
111112
import System.Semaphore (SemaphoreName (..))
@@ -697,7 +698,48 @@ buildAndInstallUnpackedPackage
697698
runConfigure
698699
PBBuildPhase{runBuild} -> do
699700
noticeProgress ProgressBuilding
701+
hooksDir <- (</> "cabalHooks") <$> getCurrentDirectory
702+
-- run preBuildHook. If it returns with 0, we assume the build was
703+
-- successful. If not, run the build.
704+
preBuildHookFile <- canonicalizePath (hooksDir </> "preBuildHook")
705+
existsPre <- doesFileExist preBuildHookFile
706+
preCode <-
707+
if existsPre
708+
then do
709+
assertHookHash (pkgConfigHookHashes pkgshared) preBuildHookFile
710+
rawSystemExitCode
711+
verbosity
712+
(Just srcdir)
713+
preBuildHookFile
714+
[ (unUnitId $ installedUnitId rpkg)
715+
, (getSymbolicPath srcdir)
716+
, (getSymbolicPath builddir)
717+
]
718+
Nothing
719+
`catchIO` (\_ -> pure (ExitFailure 10))
720+
else pure ExitSuccess
721+
-- Regardless of whether the preBuildHook exists or not, or whether it returned an
722+
-- error or not, we want to run the build command.
723+
-- If the preBuildHook downloads a cached version of the build products, the following
724+
-- should be a NOOP.
700725
runBuild
726+
-- not sure, if we want to care about a failed postBuildHook?
727+
postBuildHookFile <- canonicalizePath (hooksDir </> "postBuildHook")
728+
existsPost <- doesFileExist postBuildHookFile
729+
when existsPost $ do
730+
assertHookHash (pkgConfigHookHashes pkgshared) postBuildHookFile
731+
void $
732+
rawSystemExitCode
733+
verbosity
734+
(Just srcdir)
735+
postBuildHookFile
736+
[ (unUnitId $ installedUnitId rpkg)
737+
, (getSymbolicPath srcdir)
738+
, (getSymbolicPath builddir)
739+
, show preCode
740+
]
741+
Nothing
742+
`catchIO` (\_ -> pure (ExitFailure 10))
701743
PBHaddockPhase{runHaddock} -> do
702744
noticeProgress ProgressHaddock
703745
runHaddock

‎cabal-install/src/Distribution/Client/ProjectConfig/Legacy.hs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -714,7 +714,7 @@ convertLegacyAllPackageFlags globalFlags configFlags configExFlags installFlags
714714
} = globalFlags
715715

716716
projectConfigPackageDBs = (fmap . fmap) (interpretPackageDB Nothing) projectConfigPackageDBs_
717-
717+
projectConfigHookHashes = mempty -- :: Map FilePath HookAccept
718718
ConfigFlags
719719
{ configCommonFlags = commonFlags
720720
, configHcFlavor = projectConfigHcFlavor

‎cabal-install/src/Distribution/Client/ProjectConfig/Types.hs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import Distribution.Client.BuildReports.Types
3333
import Distribution.Client.Dependency.Types
3434
( PreSolver
3535
)
36+
import Distribution.Client.HookAccept (HookAccept (..))
3637
import Distribution.Client.Targets
3738
( UserConstraint
3839
)
@@ -227,6 +228,7 @@ data ProjectConfigShared = ProjectConfigShared
227228
, projectConfigPreferOldest :: Flag PreferOldest
228229
, projectConfigProgPathExtra :: NubList FilePath
229230
, projectConfigMultiRepl :: Flag Bool
231+
, projectConfigHookHashes :: Map FilePath HookAccept
230232
-- More things that only make sense for manual mode, not --local mode
231233
-- too much control!
232234
-- projectConfigShadowPkgs :: Flag Bool,

‎cabal-install/src/Distribution/Client/ProjectOrchestration.hs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,8 @@ import qualified Data.List.NonEmpty as NE
176176
import qualified Data.Map as Map
177177
import qualified Data.Set as Set
178178
import Distribution.Client.Errors
179+
import Distribution.Client.HookAccept (loadHookHasheshMap)
180+
179181
import Distribution.Package
180182
import Distribution.Simple.Command (commandShowOptions)
181183
import Distribution.Simple.Compiler
@@ -363,13 +365,16 @@ withInstallPlan
363365
, installedPackages
364366
}
365367
action = do
368+
hookHashes <- loadHookHasheshMap (projectConfigConfigFile $ projectConfigShared projectConfig)
369+
366370
-- Take the project configuration and make a plan for how to build
367371
-- everything in the project. This is independent of any specific targets
368372
-- the user has asked for.
369373
--
370374
(elaboratedPlan, _, elaboratedShared, _, _) <-
371375
rebuildInstallPlan
372376
verbosity
377+
hookHashes
373378
distDirLayout
374379
cabalDirLayout
375380
projectConfig
@@ -392,13 +397,16 @@ runProjectPreBuildPhase
392397
, installedPackages
393398
}
394399
selectPlanSubset = do
400+
hookHashes <- loadHookHasheshMap (projectConfigConfigFile $ projectConfigShared projectConfig)
401+
395402
-- Take the project configuration and make a plan for how to build
396403
-- everything in the project. This is independent of any specific targets
397404
-- the user has asked for.
398405
--
399406
(elaboratedPlan, _, elaboratedShared, _, _) <-
400407
rebuildInstallPlan
401408
verbosity
409+
hookHashes
402410
distDirLayout
403411
cabalDirLayout
404412
projectConfig

‎cabal-install/src/Distribution/Client/ProjectPlanning.hs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ import Distribution.Client.Dependency
117117
import Distribution.Client.DistDirLayout
118118
import Distribution.Client.FetchUtils
119119
import Distribution.Client.HashValue
120+
import Distribution.Client.HookAccept (HookAccept)
120121
import Distribution.Client.HttpUtils
121122
import Distribution.Client.JobControl
122123
import Distribution.Client.PackageHash
@@ -589,6 +590,7 @@ Binary ProgramDb instance.
589590
--
590591
rebuildInstallPlan
591592
:: Verbosity
593+
-> Map FilePath HookAccept
592594
-> DistDirLayout
593595
-> CabalDirLayout
594596
-> ProjectConfig
@@ -604,6 +606,7 @@ rebuildInstallPlan
604606
-- ^ @(improvedPlan, elaboratedPlan, _, _, _)@
605607
rebuildInstallPlan
606608
verbosity
609+
hookHashes
607610
distDirLayout@DistDirLayout
608611
{ distProjectRootDirectory
609612
, distProjectCacheFile
@@ -621,7 +624,7 @@ rebuildInstallPlan
621624
fileMonitorImprovedPlan
622625
-- react to changes in the project config,
623626
-- the package .cabal files and the path
624-
(projectConfigMonitored, localPackages, progsearchpath)
627+
(projectConfigMonitored, localPackages, progsearchpath, hookHashes)
625628
$ do
626629
-- And so is the elaborated plan that the improved plan based on
627630
(elaboratedPlan, elaboratedShared, totalIndexState, activeRepos) <-
@@ -631,6 +634,7 @@ rebuildInstallPlan
631634
( projectConfigMonitored
632635
, localPackages
633636
, progsearchpath
637+
, hookHashes
634638
)
635639
$ do
636640
compilerEtc <- phaseConfigureCompiler projectConfig
@@ -737,6 +741,7 @@ rebuildInstallPlan
737741
, compiler
738742
, platform
739743
, programDbSignature progdb
744+
, hookHashes
740745
)
741746
$ do
742747
installedPkgIndex <-
@@ -865,6 +870,7 @@ rebuildInstallPlan
865870
liftIO . runLogProgress verbosity $
866871
elaborateInstallPlan
867872
verbosity
873+
hookHashes
868874
platform
869875
compiler
870876
progdb
@@ -1585,6 +1591,7 @@ planPackages
15851591
-- matching that of the classic @cabal install --user@ or @--global@
15861592
elaborateInstallPlan
15871593
:: Verbosity
1594+
-> Map FilePath HookAccept
15881595
-> Platform
15891596
-> Compiler
15901597
-> ProgramDb
@@ -1602,6 +1609,7 @@ elaborateInstallPlan
16021609
-> LogProgress (ElaboratedInstallPlan, ElaboratedSharedConfig)
16031610
elaborateInstallPlan
16041611
verbosity
1612+
hookHashes
16051613
platform
16061614
compiler
16071615
compilerprogdb
@@ -1625,6 +1633,7 @@ elaborateInstallPlan
16251633
, pkgConfigCompiler = compiler
16261634
, pkgConfigCompilerProgs = compilerprogdb
16271635
, pkgConfigReplOptions = mempty
1636+
, pkgConfigHookHashes = hookHashes
16281637
}
16291638

16301639
preexistingInstantiatedPkgs :: Map UnitId FullUnitId

‎cabal-install/src/Distribution/Client/ProjectPlanning/Types.hs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ import Distribution.Client.Types
8484
import Distribution.Backpack
8585
import Distribution.Backpack.ModuleShape
8686

87+
import Distribution.Client.HookAccept (HookAccept (..))
8788
import Distribution.Compat.Graph (IsNode (..))
8889
import Distribution.InstalledPackageInfo (InstalledPackageInfo)
8990
import Distribution.ModuleName (ModuleName)
@@ -190,6 +191,7 @@ data ElaboratedSharedConfig = ElaboratedSharedConfig
190191
-- ghc & ghc-pkg). Once constructed, only the 'configuredPrograms' are
191192
-- used.
192193
, pkgConfigReplOptions :: ReplOptions
194+
, pkgConfigHookHashes :: Map FilePath HookAccept
193195
}
194196
deriving (Show, Generic)
195197

‎cabal-install/tests/IntegrationTests2.hs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2193,6 +2193,7 @@ planProject testdir cliConfig = do
21932193
(elaboratedPlan, _, elaboratedShared, _, _) <-
21942194
rebuildInstallPlan
21952195
verbosity
2196+
mempty
21962197
distDirLayout
21972198
cabalDirLayout
21982199
projectConfig

‎cabal-install/tests/UnitTests/Distribution/Client/ProjectConfig.hs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
module UnitTests.Distribution.Client.ProjectConfig (tests) where
1212

1313
import Control.Monad
14+
import qualified Data.ByteString.Lazy.Char8 as LBS
1415
import Data.Either (isRight)
1516
import Data.Foldable (for_)
1617
import Data.List (intercalate, isPrefixOf, (\\))
@@ -26,6 +27,8 @@ import System.IO.Unsafe (unsafePerformIO)
2627
import Distribution.Deprecated.ParseUtils
2728
import qualified Distribution.Deprecated.ReadP as Parse
2829

30+
import Distribution.Client.HashValue (hashValue)
31+
import Distribution.Client.HookAccept (HookAccept (..))
2932
import Distribution.Compiler
3033
import Distribution.Package
3134
import Distribution.PackageDescription
@@ -638,6 +641,7 @@ instance Arbitrary ProjectConfigShared where
638641
projectConfigPreferOldest <- arbitrary
639642
projectConfigProgPathExtra <- toNubList <$> listOf arbitraryShortToken
640643
projectConfigMultiRepl <- arbitrary
644+
projectConfigHookHashes <- arbitrary
641645
return ProjectConfigShared{..}
642646
where
643647
arbitraryConstraints :: Gen [(UserConstraint, ConstraintSource)]
@@ -684,13 +688,17 @@ instance Arbitrary ProjectConfigShared where
684688
<*> shrinker projectConfigPreferOldest
685689
<*> shrinker projectConfigProgPathExtra
686690
<*> shrinker projectConfigMultiRepl
691+
<*> shrinker projectConfigHookHashes
687692
where
688693
preShrink_Constraints = map fst
689694
postShrink_Constraints = map (\uc -> (uc, projectConfigConstraintSource))
690695

691696
projectConfigConstraintSource :: ConstraintSource
692697
projectConfigConstraintSource = ConstraintSourceProjectConfig nullProjectConfigPath
693698

699+
instance Arbitrary HookAccept where
700+
arbitrary = elements [AcceptAlways, AcceptHash (hashValue $ LBS.pack "000000")]
701+
694702
instance Arbitrary ProjectConfigProvenance where
695703
arbitrary = elements [Implicit, Explicit (ProjectConfigPath $ "cabal.project" :| [])]
696704

‎cabal-install/tests/UnitTests/Distribution/Client/TreeDiffInstances.hs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import Distribution.Solver.Types.Settings
1313
import Distribution.Client.BuildReports.Types
1414
import Distribution.Client.CmdInstall.ClientInstallFlags
1515
import Distribution.Client.Dependency.Types
16+
import Distribution.Client.HashValue (HashValue)
17+
import Distribution.Client.HookAccept (HookAccept (..))
1618
import Distribution.Client.IndexUtils.ActiveRepos
1719
import Distribution.Client.IndexUtils.IndexState
1820
import Distribution.Client.IndexUtils.Timestamp
@@ -45,6 +47,8 @@ instance ToExpr ProjectConfigPath
4547
instance ToExpr ConstraintSource
4648
instance ToExpr CountConflicts
4749
instance ToExpr FineGrainedConflicts
50+
instance ToExpr HashValue
51+
instance ToExpr HookAccept
4852
instance ToExpr IndependentGoals
4953
instance ToExpr InstallMethod
5054
instance ToExpr InstallOutcome

‎changelog.d/pr-10799

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
synopsis: Add pre and post build hooks
2+
packages: cabal-install
3+
prs: #10799
4+
issues: #9892
5+
significance: significant
6+
7+
description: {
8+
9+
- Run a program (named "preBuildHook") before doing a package build and another program
10+
(named "postBuildHook") after the package is built.
11+
- These programs are project local and need to be in the `cabalHooks` directory which is
12+
in the same directory as the `cabal.project` file.
13+
- The absence of these programs will be ignored.
14+
- How to check and run these hooks securely is specified in the documentation.
15+
}

‎doc/build-hooks.rst

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
Build Hooks
2+
===========
3+
4+
Build hooks are programs that are run before (pre-build hook) and
5+
after (post-build hook) a package (including package dependencies)
6+
is built. The hooks are completely generic and can even be absent
7+
(their absence is ignored). Regardless of the return code of the
8+
pre-build hook, the normal build is executed. In the case where
9+
the pre-build hook provides a pre-built version of what the build
10+
step would provide, the build step is still run, but should be
11+
little more than a NOOP.
12+
13+
Build hooks are project local rather than global to the user
14+
because a single user may want to use one set of hooks in one
15+
project and another set of hooks (or even none at all) for another
16+
project.
17+
18+
Since the hook files are project local (and hence likely to be committed
19+
to revision control), a naive implementation of these hooks would be
20+
a potential security issue. A solution to this potential security
21+
issue has been implemented and described in the Hook Security section
22+
below.
23+
24+
25+
Possible Use Cases
26+
------------------
27+
28+
Possible use cases include:
29+
30+
* Fine grained benchmarking of individual package build times.
31+
* Build product caching.
32+
33+
34+
Location of Hook Files
35+
----------------------
36+
37+
The two hook files are `cabalHooks/preBuildHook` and
38+
`cabalHooks/postBuildHook` where the `cabalHooks` directory is in
39+
the same directory as the `cabal.project` file. On UNIX style
40+
systems, these hooks need to be marked as user executable programs.
41+
42+
43+
Hook Parameters Exit Codes
44+
--------------------------
45+
46+
The pre-build hook is passed three parameters; the unit id (from cabal),
47+
the source directory and the build directory. The post-build hook is
48+
passed the same three parameters, plus the exit code of the pre-build
49+
hook.
50+
51+
The exit codes for the two hooks are ignored by cabal apart from cabal
52+
capturing the exit code for the pre-build hook and passing it to the
53+
post-build hook.
54+
55+
56+
Hook Security
57+
-------------
58+
These hook files are generic executable programs and are stored local to
59+
the project. To prevent the running of malicious hook files, the
60+
hook files are only run, it they are mentioned in the `hooks-security`
61+
file which is located in the users home `.cabal` directory, usually
62+
either `$HOME/.cabal/` or `$HOME/.config/cabal/`.
63+
64+
The `hooks-security` file should contain one entry per line. Blank lines
65+
are ignored, as are Haskell style single line comments (starts with "--"
66+
and goes until the end of the line). Each entry should contain the full
67+
hook file path, at least one space and either "AcceptAlways" or
68+
"AcceptHash" followed by at least one space and the hexadecimal encoded
69+
hash of the file. The `hooks-security` file is read once when `cabal` is
70+
started and the entries inserted into a `Map`.
71+
72+
When `cabal` detects a "preBuildHook" or "postBuildHook" it looks up
73+
the full file path in the `Map`. If the path is not found in the `Map`,
74+
`cabal` will die with an error message suggesting that the hook file
75+
be manually inspected and if deemed safe, added to the `hooks-security`
76+
file.
77+
78+
If the hook file path is in the `Map` and it was specified as
79+
"AcceptAlways" the hook will be run. If the `Map` entry is "AcceptHash"
80+
with a hash, the hash of the hook file will be calculated and compared
81+
against the supplied hash. If the hashes match, the hook will be run.
82+
If there is a hash mismatch, `cabal` will abort with an error message
83+
about the hash mismatch.

‎doc/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ Welcome to the Cabal User Guide
2020
how-to-run-in-windows
2121
how-to-use-backpack
2222
how-to-report-bugs
23+
build-hooks
2324

2425
.. toctree::
2526
:caption: Cabal Reference

0 commit comments

Comments
 (0)
Please sign in to comment.