diff --git a/cabal-install/cabal-install.cabal b/cabal-install/cabal-install.cabal index c083a8d18ef..c25cfde128f 100644 --- a/cabal-install/cabal-install.cabal +++ b/cabal-install/cabal-install.cabal @@ -141,6 +141,7 @@ library Distribution.Client.GlobalFlags Distribution.Client.Haddock Distribution.Client.HashValue + Distribution.Client.HookAccept Distribution.Client.HttpUtils Distribution.Client.IndexUtils Distribution.Client.IndexUtils.ActiveRepos diff --git a/cabal-install/src/Distribution/Client/CmdFreeze.hs b/cabal-install/src/Distribution/Client/CmdFreeze.hs index 29718b5d441..d6dae289354 100644 --- a/cabal-install/src/Distribution/Client/CmdFreeze.hs +++ b/cabal-install/src/Distribution/Client/CmdFreeze.hs @@ -142,6 +142,7 @@ freezeAction flags@NixStyleFlags{..} extraArgs globalFlags = do (_, elaboratedPlan, _, totalIndexState, activeRepos) <- rebuildInstallPlan verbosity + mempty distDirLayout cabalDirLayout projectConfig diff --git a/cabal-install/src/Distribution/Client/CmdTarget.hs b/cabal-install/src/Distribution/Client/CmdTarget.hs index c2edeeec89c..4c9cc59ec24 100644 --- a/cabal-install/src/Distribution/Client/CmdTarget.hs +++ b/cabal-install/src/Distribution/Client/CmdTarget.hs @@ -160,6 +160,7 @@ targetAction flags@NixStyleFlags{..} ts globalFlags = do (_, elaboratedPlan, _, _, _) <- rebuildInstallPlan verbosity + mempty distDirLayout cabalDirLayout projectConfig diff --git a/cabal-install/src/Distribution/Client/Errors.hs b/cabal-install/src/Distribution/Client/Errors.hs index ff9ad369bef..85cc3ae33c0 100644 --- a/cabal-install/src/Distribution/Client/Errors.hs +++ b/cabal-install/src/Distribution/Client/Errors.hs @@ -186,6 +186,8 @@ data CabalInstallException | MissingPackageList Repo.RemoteRepo | CmdPathAcceptsNoTargets | CmdPathCommandDoesn'tSupportDryRun + | HookAcceptUnknown FilePath FilePath String + | HookAcceptHashMismatch FilePath FilePath String String deriving (Show) exceptionCodeCabalInstall :: CabalInstallException -> Int @@ -338,6 +340,8 @@ exceptionCodeCabalInstall e = case e of MissingPackageList{} -> 7160 CmdPathAcceptsNoTargets{} -> 7161 CmdPathCommandDoesn'tSupportDryRun -> 7163 + HookAcceptUnknown{} -> 7164 + HookAcceptHashMismatch{} -> 7165 exceptionMessageCabalInstall :: CabalInstallException -> String exceptionMessageCabalInstall e = case e of @@ -860,6 +864,36 @@ exceptionMessageCabalInstall e = case e of "The 'path' command accepts no target arguments." CmdPathCommandDoesn'tSupportDryRun -> "The 'path' command doesn't support the flag '--dry-run'." + HookAcceptUnknown hsPath fpath hash -> + concat + [ "The following file does not appear in the hooks-security file.\n" + , " hook file : " + , fpath + , "\n" + , " file hash : " + , hash + , "\n" + , "After checking the contents of that file, it should be added to the\n" + , "hooks-security file with either AcceptAlways or better yet an AcceptHash.\n" + , "The hooks-security file is (probably) located at: " + , hsPath + ] + HookAcceptHashMismatch hsPath fpath expected actual -> + concat + [ "\nHook file hash mismatch for:\n" + , " hook file : " + , fpath + , "\n" + , " expected hash: " + , expected + , "\n" + , " actual hash : " + , actual + , "\n" + , "The hook file should be inspected and if deemed ok, the hooks-security file updated.\n" + , "The hooks-security file is (probably) located at: " + , hsPath + ] instance Exception (VerboseException CabalInstallException) where displayException :: VerboseException CabalInstallException -> [Char] diff --git a/cabal-install/src/Distribution/Client/HashValue.hs b/cabal-install/src/Distribution/Client/HashValue.hs index c5698f27f1e..12616e07eaa 100644 --- a/cabal-install/src/Distribution/Client/HashValue.hs +++ b/cabal-install/src/Distribution/Client/HashValue.hs @@ -4,6 +4,7 @@ module Distribution.Client.HashValue ( HashValue , hashValue + , hashValueFromHex , truncateHash , showHashValue , readFileHashValue @@ -51,6 +52,11 @@ instance Structured HashValue hashValue :: LBS.ByteString -> HashValue hashValue = HashValue . SHA256.hashlazy +-- From a base16 encoded Bytestring to a HashValue with `Base16`'s +-- error passing through. +hashValueFromHex :: BS.ByteString -> Either String HashValue +hashValueFromHex bs = HashValue <$> Base16.decode bs + showHashValue :: HashValue -> String showHashValue (HashValue digest) = BS.unpack (Base16.encode digest) diff --git a/cabal-install/src/Distribution/Client/HookAccept.hs b/cabal-install/src/Distribution/Client/HookAccept.hs new file mode 100644 index 00000000000..fc7e5ae0460 --- /dev/null +++ b/cabal-install/src/Distribution/Client/HookAccept.hs @@ -0,0 +1,97 @@ +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE OverloadedStrings #-} + +module Distribution.Client.HookAccept + ( HookAccept (..) + , assertHookHash + , loadHookHasheshMap + , parseHooks + ) where + +import Distribution.Client.Compat.Prelude + +import Data.ByteString.Char8 (ByteString) +import qualified Data.ByteString.Char8 as BS + +import qualified Data.Map.Strict as Map + +import Distribution.Client.Config (getConfigFilePath) +import Distribution.Client.Errors (CabalInstallException (..)) +import Distribution.Client.HashValue (HashValue, hashValueFromHex, readFileHashValue, showHashValue) +import Distribution.Simple.Setup (Flag (..)) +import Distribution.Simple.Utils (dieWithException) +import Distribution.Verbosity (normal) + +import System.FilePath (takeDirectory, ()) + +data HookAccept + = AcceptAlways + | AcceptHash HashValue + deriving (Eq, Show, Generic) + +instance Monoid HookAccept where + mempty = AcceptAlways -- Should never be needed. + mappend = (<>) + +instance Semigroup HookAccept where + AcceptAlways <> AcceptAlways = AcceptAlways + AcceptAlways <> AcceptHash h = AcceptHash h + AcceptHash h <> AcceptAlways = AcceptHash h + AcceptHash h <> _ = AcceptHash h + +instance Binary HookAccept +instance Structured HookAccept + +assertHookHash :: Map FilePath HookAccept -> FilePath -> IO () +assertHookHash m fpath = do + actualHash <- readFileHashValue fpath + hsPath <- getHooksSecurityFilePath NoFlag + case Map.lookup fpath m of + Nothing -> + dieWithException normal $ + HookAcceptUnknown hsPath fpath (showHashValue actualHash) + Just AcceptAlways -> pure () + Just (AcceptHash expectedHash) -> + when (actualHash /= expectedHash) $ + dieWithException normal $ + HookAcceptHashMismatch + hsPath + fpath + (showHashValue expectedHash) + (showHashValue actualHash) + +getHooksSecurityFilePath :: Flag FilePath -> IO FilePath +getHooksSecurityFilePath configFileFlag = do + hfpath <- getConfigFilePath configFileFlag + pure $ takeDirectory hfpath "hooks-security" + +loadHookHasheshMap :: Flag FilePath -> IO (Map FilePath HookAccept) +loadHookHasheshMap configFileFlag = do + hookFilePath <- getHooksSecurityFilePath configFileFlag + handleNotExists $ fmap parseHooks (BS.readFile hookFilePath) + where + handleNotExists :: IO (Map FilePath HookAccept) -> IO (Map FilePath HookAccept) + handleNotExists action = catchIO action $ \_ -> return mempty + +parseHooks :: ByteString -> Map FilePath HookAccept +parseHooks = Map.fromList . map parse . cleanUp . BS.lines + where + cleanUp :: [ByteString] -> [ByteString] + cleanUp = filter (not . BS.null) . map rmComments + + rmComments :: ByteString -> ByteString + rmComments = fst . BS.breakSubstring "--" + +parse :: ByteString -> (FilePath, HookAccept) +parse bs = + case BS.words bs of + [fp, "AcceptAlways"] -> (BS.unpack fp, AcceptAlways) + [fp, "AcceptHash"] -> buildAcceptHash fp "00" + [fp, "AcceptHash", h] -> buildAcceptHash fp h + _ -> error $ "Not able to parse:" ++ show bs + where + buildAcceptHash :: ByteString -> ByteString -> (FilePath, HookAccept) + buildAcceptHash fp h = + case hashValueFromHex h of + Left err -> error $ "Distribution.Client.HookAccept.parse :" ++ err + Right hv -> (BS.unpack fp, AcceptHash hv) diff --git a/cabal-install/src/Distribution/Client/ProjectBuilding/UnpackedPackage.hs b/cabal-install/src/Distribution/Client/ProjectBuilding/UnpackedPackage.hs index 1d2a86bea46..5f1f94d264f 100644 --- a/cabal-install/src/Distribution/Client/ProjectBuilding/UnpackedPackage.hs +++ b/cabal-install/src/Distribution/Client/ProjectBuilding/UnpackedPackage.hs @@ -30,6 +30,7 @@ module Distribution.Client.ProjectBuilding.UnpackedPackage import Distribution.Client.Compat.Prelude import Prelude () +import Distribution.Client.HookAccept (assertHookHash) import Distribution.Client.PackageHash (renderPackageHashInputs) import Distribution.Client.ProjectBuilding.Types import Distribution.Client.ProjectConfig @@ -105,7 +106,7 @@ import qualified Data.ByteString.Lazy.Char8 as LBS.Char8 import qualified Data.List.NonEmpty as NE import Control.Exception (ErrorCall, Handler (..), SomeAsyncException, assert, catches, onException) -import System.Directory (canonicalizePath, createDirectoryIfMissing, doesDirectoryExist, doesFileExist, removeFile) +import System.Directory (canonicalizePath, createDirectoryIfMissing, doesDirectoryExist, doesFileExist, getCurrentDirectory, removeFile) import System.FilePath (dropDrive, normalise, takeDirectory, (<.>), ()) import System.IO (Handle, IOMode (AppendMode), withFile) import System.Semaphore (SemaphoreName (..)) @@ -697,7 +698,48 @@ buildAndInstallUnpackedPackage runConfigure PBBuildPhase{runBuild} -> do noticeProgress ProgressBuilding + hooksDir <- ( "cabalHooks") <$> getCurrentDirectory + -- run preBuildHook. If it returns with 0, we assume the build was + -- successful. If not, run the build. + preBuildHookFile <- canonicalizePath (hooksDir "preBuildHook") + existsPre <- doesFileExist preBuildHookFile + preCode <- + if existsPre + then do + assertHookHash (pkgConfigHookHashes pkgshared) preBuildHookFile + rawSystemExitCode + verbosity + (Just srcdir) + preBuildHookFile + [ (unUnitId $ installedUnitId rpkg) + , (getSymbolicPath srcdir) + , (getSymbolicPath builddir) + ] + Nothing + `catchIO` (\_ -> pure (ExitFailure 10)) + else pure ExitSuccess + -- Regardless of whether the preBuildHook exists or not, or whether it returned an + -- error or not, we want to run the build command. + -- If the preBuildHook downloads a cached version of the build products, the following + -- should be a NOOP. runBuild + -- not sure, if we want to care about a failed postBuildHook? + postBuildHookFile <- canonicalizePath (hooksDir "postBuildHook") + existsPost <- doesFileExist postBuildHookFile + when existsPost $ do + assertHookHash (pkgConfigHookHashes pkgshared) postBuildHookFile + void $ + rawSystemExitCode + verbosity + (Just srcdir) + postBuildHookFile + [ (unUnitId $ installedUnitId rpkg) + , (getSymbolicPath srcdir) + , (getSymbolicPath builddir) + , show preCode + ] + Nothing + `catchIO` (\_ -> pure (ExitFailure 10)) PBHaddockPhase{runHaddock} -> do noticeProgress ProgressHaddock runHaddock diff --git a/cabal-install/src/Distribution/Client/ProjectConfig/Legacy.hs b/cabal-install/src/Distribution/Client/ProjectConfig/Legacy.hs index 03c1fc8f183..43015f820ea 100644 --- a/cabal-install/src/Distribution/Client/ProjectConfig/Legacy.hs +++ b/cabal-install/src/Distribution/Client/ProjectConfig/Legacy.hs @@ -714,7 +714,7 @@ convertLegacyAllPackageFlags globalFlags configFlags configExFlags installFlags } = globalFlags projectConfigPackageDBs = (fmap . fmap) (interpretPackageDB Nothing) projectConfigPackageDBs_ - + projectConfigHookHashes = mempty -- :: Map FilePath HookAccept ConfigFlags { configCommonFlags = commonFlags , configHcFlavor = projectConfigHcFlavor diff --git a/cabal-install/src/Distribution/Client/ProjectConfig/Types.hs b/cabal-install/src/Distribution/Client/ProjectConfig/Types.hs index 1a2b6ae2fa6..4f1558fa215 100644 --- a/cabal-install/src/Distribution/Client/ProjectConfig/Types.hs +++ b/cabal-install/src/Distribution/Client/ProjectConfig/Types.hs @@ -33,6 +33,7 @@ import Distribution.Client.BuildReports.Types import Distribution.Client.Dependency.Types ( PreSolver ) +import Distribution.Client.HookAccept (HookAccept (..)) import Distribution.Client.Targets ( UserConstraint ) @@ -227,6 +228,7 @@ data ProjectConfigShared = ProjectConfigShared , projectConfigPreferOldest :: Flag PreferOldest , projectConfigProgPathExtra :: NubList FilePath , projectConfigMultiRepl :: Flag Bool + , projectConfigHookHashes :: Map FilePath HookAccept -- More things that only make sense for manual mode, not --local mode -- too much control! -- projectConfigShadowPkgs :: Flag Bool, diff --git a/cabal-install/src/Distribution/Client/ProjectOrchestration.hs b/cabal-install/src/Distribution/Client/ProjectOrchestration.hs index a14d43e4b99..9907193458d 100644 --- a/cabal-install/src/Distribution/Client/ProjectOrchestration.hs +++ b/cabal-install/src/Distribution/Client/ProjectOrchestration.hs @@ -176,6 +176,8 @@ import qualified Data.List.NonEmpty as NE import qualified Data.Map as Map import qualified Data.Set as Set import Distribution.Client.Errors +import Distribution.Client.HookAccept (loadHookHasheshMap) + import Distribution.Package import Distribution.Simple.Command (commandShowOptions) import Distribution.Simple.Compiler @@ -363,6 +365,8 @@ withInstallPlan , installedPackages } action = do + hookHashes <- loadHookHasheshMap (projectConfigConfigFile $ projectConfigShared projectConfig) + -- Take the project configuration and make a plan for how to build -- everything in the project. This is independent of any specific targets -- the user has asked for. @@ -370,6 +374,7 @@ withInstallPlan (elaboratedPlan, _, elaboratedShared, _, _) <- rebuildInstallPlan verbosity + hookHashes distDirLayout cabalDirLayout projectConfig @@ -392,6 +397,8 @@ runProjectPreBuildPhase , installedPackages } selectPlanSubset = do + hookHashes <- loadHookHasheshMap (projectConfigConfigFile $ projectConfigShared projectConfig) + -- Take the project configuration and make a plan for how to build -- everything in the project. This is independent of any specific targets -- the user has asked for. @@ -399,6 +406,7 @@ runProjectPreBuildPhase (elaboratedPlan, _, elaboratedShared, _, _) <- rebuildInstallPlan verbosity + hookHashes distDirLayout cabalDirLayout projectConfig diff --git a/cabal-install/src/Distribution/Client/ProjectPlanning.hs b/cabal-install/src/Distribution/Client/ProjectPlanning.hs index c04bca730d7..7e10677437c 100644 --- a/cabal-install/src/Distribution/Client/ProjectPlanning.hs +++ b/cabal-install/src/Distribution/Client/ProjectPlanning.hs @@ -117,6 +117,7 @@ import Distribution.Client.Dependency import Distribution.Client.DistDirLayout import Distribution.Client.FetchUtils import Distribution.Client.HashValue +import Distribution.Client.HookAccept (HookAccept) import Distribution.Client.HttpUtils import Distribution.Client.JobControl import Distribution.Client.PackageHash @@ -589,6 +590,7 @@ Binary ProgramDb instance. -- rebuildInstallPlan :: Verbosity + -> Map FilePath HookAccept -> DistDirLayout -> CabalDirLayout -> ProjectConfig @@ -604,6 +606,7 @@ rebuildInstallPlan -- ^ @(improvedPlan, elaboratedPlan, _, _, _)@ rebuildInstallPlan verbosity + hookHashes distDirLayout@DistDirLayout { distProjectRootDirectory , distProjectCacheFile @@ -621,7 +624,7 @@ rebuildInstallPlan fileMonitorImprovedPlan -- react to changes in the project config, -- the package .cabal files and the path - (projectConfigMonitored, localPackages, progsearchpath) + (projectConfigMonitored, localPackages, progsearchpath, hookHashes) $ do -- And so is the elaborated plan that the improved plan based on (elaboratedPlan, elaboratedShared, totalIndexState, activeRepos) <- @@ -631,6 +634,7 @@ rebuildInstallPlan ( projectConfigMonitored , localPackages , progsearchpath + , hookHashes ) $ do compilerEtc <- phaseConfigureCompiler projectConfig @@ -737,6 +741,7 @@ rebuildInstallPlan , compiler , platform , programDbSignature progdb + , hookHashes ) $ do installedPkgIndex <- @@ -865,6 +870,7 @@ rebuildInstallPlan liftIO . runLogProgress verbosity $ elaborateInstallPlan verbosity + hookHashes platform compiler progdb @@ -1585,6 +1591,7 @@ planPackages -- matching that of the classic @cabal install --user@ or @--global@ elaborateInstallPlan :: Verbosity + -> Map FilePath HookAccept -> Platform -> Compiler -> ProgramDb @@ -1602,6 +1609,7 @@ elaborateInstallPlan -> LogProgress (ElaboratedInstallPlan, ElaboratedSharedConfig) elaborateInstallPlan verbosity + hookHashes platform compiler compilerprogdb @@ -1625,6 +1633,7 @@ elaborateInstallPlan , pkgConfigCompiler = compiler , pkgConfigCompilerProgs = compilerprogdb , pkgConfigReplOptions = mempty + , pkgConfigHookHashes = hookHashes } preexistingInstantiatedPkgs :: Map UnitId FullUnitId diff --git a/cabal-install/src/Distribution/Client/ProjectPlanning/Types.hs b/cabal-install/src/Distribution/Client/ProjectPlanning/Types.hs index 7ee5cb52f41..484c4cad297 100644 --- a/cabal-install/src/Distribution/Client/ProjectPlanning/Types.hs +++ b/cabal-install/src/Distribution/Client/ProjectPlanning/Types.hs @@ -84,6 +84,7 @@ import Distribution.Client.Types import Distribution.Backpack import Distribution.Backpack.ModuleShape +import Distribution.Client.HookAccept (HookAccept (..)) import Distribution.Compat.Graph (IsNode (..)) import Distribution.InstalledPackageInfo (InstalledPackageInfo) import Distribution.ModuleName (ModuleName) @@ -190,6 +191,7 @@ data ElaboratedSharedConfig = ElaboratedSharedConfig -- ghc & ghc-pkg). Once constructed, only the 'configuredPrograms' are -- used. , pkgConfigReplOptions :: ReplOptions + , pkgConfigHookHashes :: Map FilePath HookAccept } deriving (Show, Generic) diff --git a/cabal-install/tests/IntegrationTests2.hs b/cabal-install/tests/IntegrationTests2.hs index 2b25a64b6be..800cb6c7a44 100644 --- a/cabal-install/tests/IntegrationTests2.hs +++ b/cabal-install/tests/IntegrationTests2.hs @@ -2193,6 +2193,7 @@ planProject testdir cliConfig = do (elaboratedPlan, _, elaboratedShared, _, _) <- rebuildInstallPlan verbosity + mempty distDirLayout cabalDirLayout projectConfig diff --git a/cabal-install/tests/UnitTests.hs b/cabal-install/tests/UnitTests.hs index 8434f623e82..61def683e99 100644 --- a/cabal-install/tests/UnitTests.hs +++ b/cabal-install/tests/UnitTests.hs @@ -27,7 +27,16 @@ import qualified UnitTests.Distribution.Solver.Types.OptionalStanza main :: IO () main = do - initTests <- UnitTests.Distribution.Client.Init.tests + initTests <- UnitTests.Distribution.Client.Init.tests + if True then + defaultMain $ + testGroup + "Unit Tests" + [ testGroup + "UnitTests.Distribution.Client.ProjectConfig" + UnitTests.Distribution.Client.ProjectConfig.tests + ] + else do defaultMain $ testGroup "Unit Tests" diff --git a/cabal-install/tests/UnitTests/Distribution/Client/ProjectConfig.hs b/cabal-install/tests/UnitTests/Distribution/Client/ProjectConfig.hs index bf69b20ee04..150ab55e393 100644 --- a/cabal-install/tests/UnitTests/Distribution/Client/ProjectConfig.hs +++ b/cabal-install/tests/UnitTests/Distribution/Client/ProjectConfig.hs @@ -11,7 +11,8 @@ module UnitTests.Distribution.Client.ProjectConfig (tests) where import Control.Monad -import Data.Either (isRight) +import qualified Data.ByteString.Lazy.Char8 as LBS +import Data.Either (fromRight, isRight) import Data.Foldable (for_) import Data.List (intercalate, isPrefixOf, (\\)) import Data.List.NonEmpty (NonEmpty (..)) @@ -26,6 +27,8 @@ import System.IO.Unsafe (unsafePerformIO) import Distribution.Deprecated.ParseUtils import qualified Distribution.Deprecated.ReadP as Parse +import Distribution.Client.HashValue (hashValueFromHex) +import Distribution.Client.HookAccept (HookAccept (..)) import Distribution.Compiler import Distribution.Package import Distribution.PackageDescription @@ -638,6 +641,7 @@ instance Arbitrary ProjectConfigShared where projectConfigPreferOldest <- arbitrary projectConfigProgPathExtra <- toNubList <$> listOf arbitraryShortToken projectConfigMultiRepl <- arbitrary + projectConfigHookHashes <- arbitrary return ProjectConfigShared{..} where arbitraryConstraints :: Gen [(UserConstraint, ConstraintSource)] @@ -684,6 +688,7 @@ instance Arbitrary ProjectConfigShared where <*> shrinker projectConfigPreferOldest <*> shrinker projectConfigProgPathExtra <*> shrinker projectConfigMultiRepl + <*> shrinker projectConfigHookHashes where preShrink_Constraints = map fst postShrink_Constraints = map (\uc -> (uc, projectConfigConstraintSource)) @@ -691,6 +696,9 @@ instance Arbitrary ProjectConfigShared where projectConfigConstraintSource :: ConstraintSource projectConfigConstraintSource = ConstraintSourceProjectConfig nullProjectConfigPath +instance Arbitrary HookAccept where + arbitrary = elements [AcceptAlways] -- , AcceptHash (fromRight (HashValue "\0") $ hashValueFromHex "000000")] + instance Arbitrary ProjectConfigProvenance where arbitrary = elements [Implicit, Explicit (ProjectConfigPath $ "cabal.project" :| [])] diff --git a/cabal-install/tests/UnitTests/Distribution/Client/TreeDiffInstances.hs b/cabal-install/tests/UnitTests/Distribution/Client/TreeDiffInstances.hs index 179fef5688a..42c38a7bc82 100644 --- a/cabal-install/tests/UnitTests/Distribution/Client/TreeDiffInstances.hs +++ b/cabal-install/tests/UnitTests/Distribution/Client/TreeDiffInstances.hs @@ -13,6 +13,8 @@ import Distribution.Solver.Types.Settings import Distribution.Client.BuildReports.Types import Distribution.Client.CmdInstall.ClientInstallFlags import Distribution.Client.Dependency.Types +import Distribution.Client.HashValue (HashValue) +import Distribution.Client.HookAccept (HookAccept (..)) import Distribution.Client.IndexUtils.ActiveRepos import Distribution.Client.IndexUtils.IndexState import Distribution.Client.IndexUtils.Timestamp @@ -45,6 +47,8 @@ instance ToExpr ProjectConfigPath instance ToExpr ConstraintSource instance ToExpr CountConflicts instance ToExpr FineGrainedConflicts +instance ToExpr HashValue +instance ToExpr HookAccept instance ToExpr IndependentGoals instance ToExpr InstallMethod instance ToExpr InstallOutcome diff --git a/changelog.d/pr-10799 b/changelog.d/pr-10799 new file mode 100644 index 00000000000..3f37124aebb --- /dev/null +++ b/changelog.d/pr-10799 @@ -0,0 +1,15 @@ +synopsis: Add pre and post build hooks +packages: cabal-install +prs: #10799 +issues: #9892 +significance: significant + +description: { + +- Run a program (named "preBuildHook") before doing a package build and another program + (named "postBuildHook") after the package is built. +- These programs are project local and need to be in the `cabalHooks` directory which is + in the same directory as the `cabal.project` file. +- The absence of these programs will be ignored. +- How to check and run these hooks securely is specified in the documentation. +} diff --git a/doc/build-hooks.rst b/doc/build-hooks.rst new file mode 100644 index 00000000000..82e53693dc4 --- /dev/null +++ b/doc/build-hooks.rst @@ -0,0 +1,83 @@ +Build Hooks +=========== + +Build hooks are programs that are run before (pre-build hook) and +after (post-build hook) a package (including package dependencies) +is built. The hooks are completely generic and can even be absent +(their absence is ignored). Regardless of the return code of the +pre-build hook, the normal build is executed. In the case where +the pre-build hook provides a pre-built version of what the build +step would provide, the build step is still run, but should be +little more than a NOOP. + +Build hooks are project local rather than global to the user +because a single user may want to use one set of hooks in one +project and another set of hooks (or even none at all) for another +project. + +Since the hook files are project local (and hence likely to be committed +to revision control), a naive implementation of these hooks would be +a potential security issue. A solution to this potential security +issue has been implemented and described in the Hook Security section +below. + + +Possible Use Cases +------------------ + +Possible use cases include: + +* Fine grained benchmarking of individual package build times. +* Build product caching. + + +Location of Hook Files +---------------------- + +The two hook files are `cabalHooks/preBuildHook` and +`cabalHooks/postBuildHook` where the `cabalHooks` directory is in +the same directory as the `cabal.project` file. On UNIX style +systems, these hooks need to be marked as user executable programs. + + +Hook Parameters Exit Codes +-------------------------- + +The pre-build hook is passed three parameters; the unit id (from cabal), +the source directory and the build directory. The post-build hook is +passed the same three parameters, plus the exit code of the pre-build +hook. + +The exit codes for the two hooks are ignored by cabal apart from cabal +capturing the exit code for the pre-build hook and passing it to the +post-build hook. + + +Hook Security +------------- +These hook files are generic executable programs and are stored local to +the project. To prevent the running of malicious hook files, the +hook files are only run, it they are mentioned in the `hooks-security` +file which is located in the users home `.cabal` directory, usually +either `$HOME/.cabal/` or `$HOME/.config/cabal/`. + +The `hooks-security` file should contain one entry per line. Blank lines +are ignored, as are Haskell style single line comments (starts with "--" +and goes until the end of the line). Each entry should contain the full +hook file path, at least one space and either "AcceptAlways" or +"AcceptHash" followed by at least one space and the hexadecimal encoded +hash of the file. The `hooks-security` file is read once when `cabal` is +started and the entries inserted into a `Map`. + +When `cabal` detects a "preBuildHook" or "postBuildHook" it looks up +the full file path in the `Map`. If the path is not found in the `Map`, +`cabal` will die with an error message suggesting that the hook file +be manually inspected and if deemed safe, added to the `hooks-security` +file. + +If the hook file path is in the `Map` and it was specified as +"AcceptAlways" the hook will be run. If the `Map` entry is "AcceptHash" +with a hash, the hash of the hook file will be calculated and compared +against the supplied hash. If the hashes match, the hook will be run. +If there is a hash mismatch, `cabal` will abort with an error message +about the hash mismatch. diff --git a/doc/index.rst b/doc/index.rst index 4bd13c65d7a..a721b77ed63 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -20,6 +20,7 @@ Welcome to the Cabal User Guide how-to-run-in-windows how-to-use-backpack how-to-report-bugs + build-hooks .. toctree:: :caption: Cabal Reference