packages feed

cabal-buck2-0.2.0.0: src/Distribution/Client/Buck2/Cache.hs

-- | Using a remote action cache, so that buck2 can reuse the results of
-- actions that were run before, in another checkout of the project or by an
-- earlier build with the same inputs.
--
-- Nothing is run remotely: the cache server (anything that speaks the Bazel
-- remote execution API's action cache and CAS, such as @bazel-remote@) only
-- stores results. Turning this on adds a block to the project's
-- @.buckconfig@ (see 'cacheBlock'); the buck2 support code reads
-- @[cabal_buck2] cache@ from it to enable the cache in its execution platform
-- and C\/C++ toolchain.
module Distribution.Client.Buck2.Cache
  ( CacheSetting (..)
  , cacheSetting
  , configureCache
  , cacheBlock
  , spliceBlock
  ) where

import Distribution.Client.Compat.Prelude
import Prelude ()

import System.Directory (doesFileExist)
import System.FilePath ((</>))

import Distribution.Simple.Flag (flagToMaybe, fromFlagOrDefault)
import Distribution.Simple.Utils (die', notice)

import Distribution.Client.Buck2.Flags (Buck2Flags (..))

data CacheSetting
  = -- | Leave .buckconfig as it is.
    CacheUnchanged
  | CacheOn String
  | CacheOff
  deriving (Eq, Show)

-- | What the flags ask for, or an error if they contradict each other.
cacheSetting :: Buck2Flags -> Either String CacheSetting
cacheSetting flags =
  case (flagToMaybe (buck2CacheAddress flags), fromFlagOrDefault False (buck2NoCache flags)) of
    (Just _, True) -> Left "--cache and --no-cache can't be used together"
    (Just address, False)
      | any (`isPrefixOf` address) ["grpc://", "grpcs://"] -> Right (CacheOn address)
      | otherwise -> Left ("--cache: the address should start with grpc:// or grpcs://, not " ++ show address)
    (Nothing, True) -> Right CacheOff
    (Nothing, False) -> Right CacheUnchanged

-- | Add, change or remove the cache's block of @.buckconfig@.
configureCache :: Verbosity -> FilePath -> CacheSetting -> IO ()
configureCache _ _ CacheUnchanged = return ()
configureCache verbosity projectRoot setting = do
  let path = projectRoot </> ".buckconfig"
  exists <- doesFileExist path
  unless exists $ die' verbosity (path ++ " doesn't exist")
  old <- readFile path
  let new = spliceBlock (case setting of CacheOn address -> Just (cacheBlock address); _ -> Nothing) old
  when (new /= old) $ do
    length old `seq` writeFile path new
    notice verbosity $
      unlines
        [ case setting of
            CacheOn address -> "cabal buck2: using the action cache at " ++ address ++ " (see .buckconfig)"
            _ -> "cabal buck2: no longer using an action cache"
        , "cabal buck2: run `buck2 kill` now: a running buck2 daemon keeps using the old settings"
        , "(with a cache address but half of the new settings it retries connecting for ~45s on every build)"
        ]

-- | The lines @cabal buck2 --cache=ADDRESS@ puts in @.buckconfig@, without the
-- markers. @default_allow_cache_upload@ is needed because most actions of
-- the prelude don't opt in to having their results uploaded, and
-- @engine_address@ because buck2 won't connect to the cache without it, even
-- though nothing is run there. buck2 reads @[buck2_re_client]@ when its daemon
-- starts, but @[cabal_buck2] cache@ on every command, so a daemon that was
-- running before these settings were added has to be restarted.
cacheBlock :: String -> [String]
cacheBlock address =
  [ "[cabal_buck2]"
  , "  cache = true"
  , ""
  , "[buck2]"
  , "  default_allow_cache_upload = true"
  , ""
  , "[buck2_re_client]"
  , "  action_cache_address = " ++ address
  , "  cas_address = " ++ address
  , "  engine_address = " ++ address
  , "  tls = " ++ (if "grpcs://" `isPrefixOf` address then "true" else "false")
  ]

beginMarker, endMarker :: String
beginMarker = "# >>> cabal buck2: cache (generated by `cabal buck2 --cache`: change it with --cache, remove it with --no-cache) >>>"
endMarker = "# <<< cabal buck2: cache <<<"

-- | Replace the block between the markers in a config file by the given
-- lines (or remove it, if there are none). If there is no block the new one
-- is added at the end. Anything outside the markers is left as it is.
spliceBlock :: Maybe [String] -> String -> String
spliceBlock block contents = case break isBegin ls of
  (before, _ : rest)
    | (_, _ : after) <- break (== endMarker) rest -> unlines ((if null new then dropSeparator before else before) ++ new ++ after)
  _ | null new -> contents
  _ -> unlines (ls ++ separator ++ new)
  where
    ls = lines contents
    isBegin = ("# >>> cabal buck2: cache" `isPrefixOf`)
    new = maybe [] (\b -> [beginMarker] ++ b ++ [endMarker]) block
    -- A blank line between what is there and the new block.
    separator = case reverse ls of
      (l : _) | any (not . isSpace) l -> [""]
      _ -> []
    -- ... which goes when the block does.
    dropSeparator xs = case reverse xs of
      (l : rest) | all isSpace l -> reverse rest
      _ -> xs