docker-build-cacher 1.8.2 → 1.9.1
raw patch · 2 files changed
+218/−141 lines, 2 filesdep ~language-docker
Dependency ranges changed: language-docker
Files
- app/Main.hs +176/−116
- docker-build-cacher.cabal +42/−25
app/Main.hs view
@@ -1,7 +1,7 @@ #!/usr/bin/env stack {- stack runghc- --resolver lts-9.14+ --resolver lts-10.2 --install-ghc --package turtle --package language-dockerfile@@ -16,14 +16,15 @@ import qualified Control.Foldl as Fold import Control.Monad (guard, when) import Data.Either (rights)+import Data.List.NonEmpty (toList) import qualified Data.Map.Strict as Map-import Data.Maybe (catMaybes, fromMaybe, isJust, mapMaybe)+import Data.Maybe (fromMaybe, isJust, mapMaybe) import qualified Data.Text as Text import Data.Text (Text) import Filesystem.Path (FilePath)-import Language.Docker hiding (Tag)+import Language.Docker hiding (Tag, workdir) import Prelude hiding (FilePath)-import Text.ParserCombinators.ReadP hiding (choice)+import Text.ParserCombinators.ReadP import Text.Read import Turtle @@ -41,16 +42,14 @@ Branch Text deriving (Show) -newtype Tag =- Tag Text- deriving (Show, Eq)+data SourceImage =+ SourceImage -newtype SourcePath =- SourcePath FilePath- deriving (Show, Eq)+data CachedImage =+ CachedImage -newtype TargetPath =- TargetPath FilePath+newtype Tag a =+ Tag Text deriving (Show, Eq) newtype BuildOptions =@@ -59,20 +58,22 @@ -- | A Stage is one of the FROM directives ina dockerfile ---data Stage = Stage+data Stage a = Stage { stageName :: Text -- The image name- , stageTag :: Tag -- The image tag+ , stageTag :: Tag a -- The image tag , stagePos :: Linenumber -- Where in the docker file this line is found- , stageAlias :: Text -- The alias used for building+ , stageAlias :: Text -- The alias in the FROM instruction+ , buildTag :: Tag a , directives :: Dockerfile -- Dockerfile is an alias for [InstructionPos]- , alreadyCached :: Bool -- Whether or not we have this stage built separately on the host } deriving (Show, Eq) data InspectedStage- = NotCached Stage -- When the image has not yet been built separetely- | Cached { stage :: Stage -- When the image was built and tagged as a separate image+ = NotCached (Stage SourceImage) -- When the image has not yet been built separetely+ | CacheNotInspected (Stage SourceImage) -- When the image is cached but not yet inspected+ | Cached { stage :: Stage CachedImage -- When the image was built and tagged as a separate image , cacheBusters :: [(SourcePath, TargetPath)] -- List of files that are able to bust the cache }+ | CacheInvalidated (Stage CachedImage) deriving (Show) -- | This script has 2 modes. One for building the Dockerfile, and another for caching its stages@@ -148,16 +149,28 @@ -- | Builds the provided Dockerfile. If it is a multi-stage build, check if those stages are already cached -- and change the dockerfile to take advantage of that.-buildFromCache :: App -> Branch -> Tag -> Maybe BuildOptions -> Dockerfile -> Shell ()+buildFromCache :: App -> Branch -> Tag SourceImage -> Maybe BuildOptions -> Dockerfile -> Shell () buildFromCache app branch tag buildOptions ast = do- changedStages <- getChangedStages app branch ast -- Inspect the dockerfile and return the stages that got their cache invalidated- let bustedStages = replaceStages (filter alreadyCached changedStages) ast -- We replace the busted stages with cached primed ones+ changedStages <- getChangedStages app branch ast -- Inspect the dockerfile and return+ -- the stages that got their cache invalidated. We need+ -- them to rewrite the docker file and replace the stages+ -- with the ones we have in local cache.+ let bustedStages = replaceStages (mapMaybe alreadyCached changedStages) ast -- We replace the busted stages+ -- with cached primed ones build app tag buildOptions bustedStages+ where+ alreadyCached (CacheInvalidated stage) = Just stage -- We only want to replace stages where the cache+ -- was invalidated by any file changes.+ alreadyCached (Cached stage _) = Just stage -- Likewise, once we have a cached stage, we need to keep using it+ -- in succesive builds, so the cache is not invalidated again.+ alreadyCached _ = Nothing -build :: App -> Tag -> Maybe BuildOptions -> Dockerfile -> Shell ()+build :: App -> Tag SourceImage -> Maybe BuildOptions -> Dockerfile -> Shell () build app tag buildOptions ast = do echo "I'll start building now the main Dockerfile"- status <- buildDockerfile app tag buildOptions ast -- Build the main docker file with the maybe changed stages+ status <- buildDockerfile app tag buildOptions ast -- Build the main docker file+ -- which may already have been rewritten to use the+ -- cached stages. case status of ExitSuccess -> echo@@ -168,22 +181,36 @@ -- can be retreived at a later point. cacheBuild :: App -> Branch -> Dockerfile -> Shell () cacheBuild app branch ast = do- changedStages <- getChangedStages app branch ast- when (changedStages /= []) $ do+ inspectedStages <- getChangedStages app branch ast -- Compare the current dockerfile with whatever we have+ -- in the cache. If there are any chages, then we will need+ -- to rebuild the cache for each of the changed stages.+ let stagesToBuildFresh = [s | NotCached s <- inspectedStages]+ let stagesToReBuild = [s | CacheInvalidated s <- inspectedStages]+ when (stagesToBuildFresh /= []) $ do+ echo "--> Let's build the cache for the first time"+ mapM_ (buildAssetStage app) stagesToBuildFresh -- Build cached images for the first time+ when (stagesToReBuild /= []) $ do echo "--> Let's make the cache great again"- mapM_ (buildAssetStage app) changedStages -- Build each of the stages so they can be reused later+ mapM_ (reBuildAssetStage app) stagesToReBuild -- Build each of the stages so they can be reused later -- | Returns a list of stages which needs to either be built separately or that did not have their cached busted -- by the introduction of new code.-getChangedStages :: App -> Branch -> Dockerfile -> Shell [Stage]+getChangedStages :: App -> Branch -> Dockerfile -> Shell [InspectedStage] getChangedStages app branch ast = do- let mainFile = last (getStages ast) -- Parse all the FROM instructions in the dockerfile- assetStages = takeWhile (/= mainFile) (getStages ast) -- Filter out the main FROM at the end- stages = mapMaybe (toStage app branch) assetStages -- Convert to Stage records, filter out errors+ let mainFile = last (getStages ast) -- Parse all the FROM instructions in the dockerfile and only+ -- keep the last FROM, which is the main stage. Anything before that+ -- is considered a cacheable stage.+ assetStages = takeWhile (/= mainFile) (getStages ast) -- Filter out the main FROM at the end and only+ -- keep the contents of the file before that instruction.+ stages = mapMaybe (toStage app branch) assetStages -- For each the for found stages, before the main+ -- FROM instruction, convert them to Stage records when (length assetStages > length stages) showAliasesWarning- -- Time to get all the stages having changed cache files- bustCacheStages <- fold (getAlreadyCached stages >>= inspectCache >>= shouldBustCache) Fold.list- return (catMaybes bustCacheStages) -- Remove the stages that did not actually change+ fold+ (getAlreadyCached stages >>= -- Find the stages that we already have in local cache+ inspectCache >>= -- Gather information to determine whether or not the cache was invalidated+ shouldBustCache -- Determine whether or not the cache was invalidated+ )+ Fold.list where showAliasesWarning = do echo "::::::WARNING::::::"@@ -195,40 +222,35 @@ -- | Check whehther or not the tag exists for each of the passed stages -- and return only those that already exist.-getAlreadyCached :: [Stage] -> Shell Stage+getAlreadyCached :: [Stage SourceImage] -> Shell InspectedStage getAlreadyCached stages = do echo "--> I'm checking whether or not the stage exists as a docker image already"- stage@Stage {stageTag} <- select stages -- foreach stages as stage- printLn ("----> Looking for image " %s) (taggedBuild stageTag)+ stage@Stage {buildTag} <- select stages -- foreach stages as stage+ let Tag tag = buildTag -- Get the raw text value for the build tag+ printLn ("----> Looking for image " %s) tag existent <- fold- (inproc- "docker"- ["image", "ls", taggedBuild stageTag, "--format", "{{.Repository}}"]- empty)+ (inproc "docker" ["image", "ls", tag, "--format", "{{.Repository}}"] empty) Fold.list -- Get the output of the command as a list of lines if existent == mempty then do echo "------> It does not exist, so I will need to build it myself later"- return (stage {alreadyCached = False})+ return (NotCached stage) else do echo "------> It already exists, so I will then check if the cache files changed"- return (stage {alreadyCached = True})+ return (CacheNotInspected stage) -- | This will inspect how an image was build and extrack the ONBUILD directives. If any of those -- instructions are copying or adding files to the build, they are considered "cache busters".-inspectCache :: Stage -> Shell InspectedStage-inspectCache stage@Stage {..} =- if alreadyCached- then return (NotCached stage)- else do- history <- imageHistory (Tag (taggedBuild stageTag))- let onBuildLines = extractOnBuild history- workdir = extractWorkdir history- parsedDirectivesWithErrors = fmap (parseString . Text.unpack) onBuildLines -- Parse each of the lines- parsedDirectives = (getFirst . rights) parsedDirectivesWithErrors -- We only keep the good lines- cacheBusters = extractCachePaths workdir parsedDirectives- return Cached {..}+inspectCache :: InspectedStage -> Shell InspectedStage+inspectCache (CacheNotInspected sourceStage@Stage {..}) = do+ history <- imageHistory buildTag+ let onBuildLines = extractOnBuild history+ workdir = extractWorkdir history+ parsedDirectivesWithErrors = fmap (parseString . Text.unpack) onBuildLines -- Parse each of the lines+ parsedDirectives = (getFirst . rights) parsedDirectivesWithErrors -- We only keep the good lines+ cacheBusters = extractCachePaths workdir parsedDirectives+ return $ Cached (toCachedStage sourceStage) cacheBusters where extractCachePaths workdir dir = let filePairs = concat (mapMaybe doExtract dir) -- Get the (source, target) pairs of files copied@@ -236,30 +258,40 @@ -- -- | Prepend a given target dir to the target path prependWorkdir workdir (source, TargetPath target) =- if relative target -- If the target path is relative, we need to prepend the workdir- then (source, TargetPath (collapse (workdir </> target))) -- Remove the ./ prefix and prepend workdir- else (source, TargetPath target)+ let dest = fromString target+ prependedDest = Text.unpack . format fp $ collapse (workdir </> dest)+ in if relative dest -- If the target path is relative, we need to prepend the workdir+ then (source, TargetPath prependedDest) -- Remove the ./ prefix and prepend workdir+ else (source, TargetPath target) -- -- | COPY allows multiple paths in the same line, we need to convert each to a separate path- doExtract (InstructionPos (Copy _ t) _ _) =- let target:allSources = (reverse . Text.words . Text.pack) t -- Make sure we get all the files in COPY- sourcePaths = fmap toSource allSources- in Just (zip sourcePaths (repeat (toTarget target)))+ doExtract (InstructionPos (Copy CopyArgs {sourcePaths, targetPath}) _ _) =+ Just (zip (toList sourcePaths) (repeat targetPath)) -- -- | This case is simpler, we only need to convert the source and target from ADD- doExtract (InstructionPos (Add fr t) _ _) =- Just [((toSource . Text.pack) fr, (toTarget . Text.pack) t)]+ doExtract (InstructionPos (Add AddArgs {sourcePaths, targetPath}) _ _) =+ Just (zip (toList sourcePaths) (repeat targetPath)) doExtract _ = Nothing getFirst (first:_) = first getFirst [] = []+-- In any other case return the same inspected stage+inspectCache c@(NotCached _) = return c+inspectCache c@(Cached _ _) = return c+inspectCache c@(CacheInvalidated _) = return c +toCachedStage :: Stage SourceImage -> Stage CachedImage+toCachedStage Stage {..} =+ let stage = Stage {..}+ Tag sTag = stageTag+ Tag bTag = buildTag+ in stage {stageTag = Tag sTag, buildTag = Tag bTag}+ -- | Here check each of the cache buster from the image and compare them with those we have locally, -- if the files do not match, then we return the stage back as a result, otherwise return Nothing.-shouldBustCache :: InspectedStage -> Shell (Maybe Stage)-shouldBustCache (NotCached stage) = return (Just stage)-shouldBustCache Cached {..} = do+shouldBustCache :: InspectedStage -> Shell InspectedStage+shouldBustCache cached@Cached {..} = do printLn ("----> Checking cache buster files for stage " %s) (stageName stage)- withContainer stage checkFiles -- Create a container to inspect the files+ withContainer (buildTag stage) checkFiles -- Create a container to inspect the files where checkFiles containerId = do hasChanged <- fold (mfilter isJust (checkFileChanged containerId cacheBusters)) Fold.head@@ -268,13 +300,15 @@ if isJust hasChanged then do printLn ("----> The stage " %s % " changed") (stageName stage)- return (Just stage)+ return (CacheInvalidated stage) else do printLn ("----> The stage " %s % " did not change") (stageName stage)- return Nothing+ return cached -- | checkFileChanged containerId files = do- (SourcePath file, TargetPath targetDir) <- select files+ (SourcePath src, TargetPath dest) <- select files+ let file = fromText $ Text.pack src+ let targetDir = fromText $ Text.pack dest printLn ("------> Checking file '" %fp % "' in directory " %fp) file targetDir currentDirectory <- pwd tempFile <- mktempfile currentDirectory "comp"@@ -290,12 +324,16 @@ if local == remote then return Nothing else return (Just file)+-- In any other case return the same inspected stage+shouldBustCache c@(NotCached _) = return c+shouldBustCache c@(CacheNotInspected _) = return c+shouldBustCache c@(CacheInvalidated _) = return c -- | Creates a container from a stage and passes the container id to the -- given shell as an argument-withContainer :: Stage -> (Text -> Shell b) -> Shell b-withContainer stage action = do- containerId <- inproc "docker" ["create", taggedBuild (stageTag stage)] empty+withContainer :: Tag a -> (Text -> Shell b) -> Shell b+withContainer (Tag tag) action = do+ containerId <- inproc "docker" ["create", tag] empty result <- fold (action (format l containerId)) Fold.list _ <- removeContainer containerId -- Ignore the return code of this command select result -- yield each result as a separate line@@ -305,24 +343,43 @@ -- | The goal is to create a temporary dockefile in this same folder with the contents -- if the stage variable, call docker build with the generated file and tag the image -- so we can find it later.-buildAssetStage :: App -> Stage -> Shell ()+buildAssetStage :: App -> Stage SourceImage -> Shell () buildAssetStage app Stage {..} = do- printLn ("\n--> Building asset stage " %s % " at line " %d) stageName stagePos+ printLn+ ("\n--> Building asset stage " %s % " at line " %d % " for the first time")+ stageName+ stagePos let filteredDirectives = filter isFrom directives- status <- buildDockerfile app stageTag Nothing filteredDirectives -- Only build the FROM+ doStageBuild app stageTag buildTag filteredDirectives+ where+ isFrom (InstructionPos (From _) _ _) = True+ isFrom _ = False++-- | The goal is to create a temporary dockefile in this same folder with the contents+-- if the stage variable, call docker build with the generated file and tag the image+-- so we can find it later.+reBuildAssetStage :: App -> Stage CachedImage -> Shell ()+reBuildAssetStage app Stage {..} = do+ printLn ("\n--> Rebuilding asset stage " %s % " at line " %d) stageName stagePos+ let fromInstruction =+ toDockerfile $ do+ let Tag t = buildTag+ from $ Text.unpack t `tagged` "latest" -- Use the cached image as base for the new one+ doStageBuild app stageTag buildTag fromInstruction++doStageBuild :: App -> Tag a -> Tag b -> Dockerfile -> Shell ()+doStageBuild app sourceTag targetTag directives = do+ status <- buildDockerfile app sourceTag Nothing directives -- Only build the FROM guard (status == ExitSuccess) -- Break if previous command failed- history <- imageHistory stageTag -- Get the commands used to build the docker image- newDockerfile <- createDockerfile stageTag (extractOnBuild history) -- Append the ONBUILD lines to the new file- finalStatus <- buildDockerfile app (Tag (taggedBuild stageTag)) Nothing newDockerfile -- Now build it+ history <- imageHistory sourceTag -- Get the commands used to build the docker image+ newDockerfile <- createDockerfile sourceTag (extractOnBuild history) -- Append the ONBUILD lines to the new file+ finalStatus <- buildDockerfile app targetTag Nothing newDockerfile -- Now build it guard (finalStatus == ExitSuccess) -- Stop here if previous command failed echo "" echo "--> I have tagged a cache container that I can use next time to speed builds!"- where- isFrom (InstructionPos (From _) _ _) = True- isFrom _ = False -- | Simply call docker build for the passed arguments-buildDockerfile :: App -> Tag -> Maybe BuildOptions -> Dockerfile -> Shell ExitCode+buildDockerfile :: App -> Tag a -> Maybe BuildOptions -> Dockerfile -> Shell ExitCode buildDockerfile (App app) (Tag tag) buildOPtions directives = do currentDirectory <- pwd tmpFile <- mktempfile currentDirectory "Dockerfile."@@ -335,7 +392,7 @@ -- | Given a list of instructions, build a dockerfile where the tag is the FROM for the file and -- the list of instructions are wrapped with ONBUILD-createDockerfile :: Tag -> [Text] -> Shell Dockerfile+createDockerfile :: Tag a -> [Text] -> Shell Dockerfile createDockerfile (Tag tag) onBuildLines = do let eitherDirectives = map (parseString . Text.unpack) onBuildLines file =@@ -380,7 +437,7 @@ isOnBuild line = Text.isPrefixOf onBuildPrefix (lineToText line) -- | Calls docker history for the given image name and returns the output as a list-imageHistory :: Tag -> Shell [Line]+imageHistory :: Tag a -> Shell [Line] imageHistory (Tag tag) = do printLn ("----> Checking the docker image history for " %s) tag out <- fold fetchHistory Fold.list -- Buffer all the output of the imageHistory shell@@ -405,20 +462,26 @@ startsWithFROM _ = False -- | Converts a list of instructions into a Stage record-toStage :: App -> Branch -> Dockerfile -> Maybe Stage+toStage :: App -> Branch -> Dockerfile -> Maybe (Stage a) toStage (App app) (Branch branch) directives = do- (stageName, stagePos, stageAlias) <- getStageInfo directives -- If getStageInfo returns Nothing, skip the rest+ (stageName, stagePos, stageAlias) <- extractInfo directives -- If getStageInfo returns Nothing, skip the rest let sanitized = sanitize stageName- stageTag = Tag (app <> "__branch__" <> branch <> "__stage__" <> sanitized)- alreadyCached = False+ tagName = app <> "__branch__" <> branch <> "__stage__" <> sanitized+ stageTag = Tag tagName+ buildTag = Tag (tagName <> "-build") return Stage {..} where- getStageInfo :: [InstructionPos] -> Maybe (Text, Linenumber, Text)- getStageInfo ((InstructionPos (From (TaggedImage name tag)) _ pos):_) =- if Text.isInfixOf " as " (Text.pack tag) || Text.isInfixOf " AS " (Text.pack tag) -- Make sure the FROM is aliased- then Just (Text.pack name, pos, parseAlias tag)- else Nothing- getStageInfo _ = Nothing+ extractInfo :: Dockerfile -> Maybe (Text, Linenumber, Text)+ extractInfo (InstructionPos {instruction, lineNumber}:_) = getStageInfo instruction lineNumber+ extractInfo _ = Nothing+ getStageInfo :: Instruction -> Linenumber -> Maybe (Text, Linenumber, Text)+ getStageInfo (From (TaggedImage name _ (Just (ImageAlias alias)))) pos =+ Just (Text.pack name, pos, Text.pack alias)+ getStageInfo (From (UntaggedImage name (Just (ImageAlias alias)))) pos =+ Just (Text.pack name, pos, Text.pack alias)+ getStageInfo (From (DigestedImage name _ (Just (ImageAlias alias)))) pos =+ Just (Text.pack name, pos, Text.pack alias)+ getStageInfo _ _ = Nothing -- -- | Makes a string safe to use it as a file name sanitize = Text.replace "/" "-" . Text.replace ":" "-"@@ -433,38 +496,35 @@ Text.replace " AS " " as " . -- Normalize AS with as Text.pack -taggedBuild :: Tag -> Text-taggedBuild (Tag tag) = tag <> "-build"- -- | Given a list of stages and the AST for a Dockerfile, replace all the FROM instructions -- with their corresponding images as described in the Stage record.-replaceStages :: [Stage] -> Dockerfile -> Dockerfile-replaceStages stages dockerLines = fmap replaceStage dockerLines+replaceStages :: [Stage CachedImage] -> Dockerfile -> Dockerfile+replaceStages stages dockerLines =+ fmap+ (\InstructionPos {..} -> InstructionPos {instruction = replaceStage instruction, ..})+ dockerLines where stagesMap = Map.fromList (map createStagePairs stages) createStagePairs stage@(Stage {..}) = (stageAlias, stage) -- -- | Find whehter or not we have extracted a stage with the same alias -- If we did, then replace the FROM directive with our own version- replaceStage directive@(InstructionPos (From (TaggedImage _ tag)) file pos) =- case Map.lookup (parseAlias tag) stagesMap of- Nothing -> directive- Just Stage {stageTag, stageAlias} ->- InstructionPos- (From- (TaggedImage (Text.unpack (taggedBuild stageTag)) (formatAlias stageAlias)))- file- pos+ replaceStage directive@(From (TaggedImage _ _ (Just (ImageAlias imageAlias)))) =+ replaceKnownAlias directive imageAlias+ replaceStage directive@(From (UntaggedImage _ (Just (ImageAlias imageAlias)))) =+ replaceKnownAlias directive imageAlias+ replaceStage directive@(From (DigestedImage _ _ (Just (ImageAlias imageAlias)))) =+ replaceKnownAlias directive imageAlias replaceStage directive = directive- formatAlias alias = "latest as " <> Text.unpack alias+ replaceKnownAlias directive imageAlias =+ case Map.lookup (Text.pack imageAlias) stagesMap of+ Nothing -> directive+ Just Stage {buildTag, stageAlias} ->+ let Tag t = buildTag+ in From (TaggedImage (Text.unpack t) "latest" (formatAlias stageAlias))+ formatAlias = Just . fromString . Text.unpack printLn message = printf (message % "\n")---- | Transforms a text to SourcePath-toSource = SourcePath . fromText---- | Transforms a text to TargetPath-toTarget = TargetPath . fromText needEnv varName = do value <- need varName
docker-build-cacher.cabal view
@@ -1,27 +1,44 @@-name: docker-build-cacher-version: 1.8.2-synopsis: Builds a services with docker and caches all of its intermediate stages-description: A CLI tool to speed up multi-stage docker file builds by caching intermediate- stages separately, so the can be used in successive builds.-license: BSD3-license-file: LICENSE-author: Jose Lorenzo Rodriguez-maintainer: lorenzo@seatgeek.com-copyright: 2017 Seatgeek-category: Operations-build-type: Simple-extra-source-files: README.md-cabal-version: >=1.10+-- This file has been generated from package.yaml by hpack version 0.20.0.+--+-- see: https://github.com/sol/hpack+--+-- hash: d1e8cccdc57c09ce235c5e44a3b5f4201d6679d623ef51b1c15db6a503e69f2c +name: docker-build-cacher+version: 1.9.1+synopsis: Builds a services with docker and caches all of its intermediate stages+description: A CLI tool to speed up multi-stage docker file builds by caching intermediate+category: Operations+homepage: https://github.com/seatgeek/docker-build-cacher#readme+bug-reports: https://github.com/seatgeek/docker-build-cacher/issues+author: José Lorenzo Rodríguez+maintainer: lorenzo@seatgeek.com+copyright: Seatgeek, Copyright (c) 2017+license: BSD3+license-file: LICENSE+build-type: Simple+cabal-version: >= 1.10++extra-source-files:+ README.md++source-repository head+ type: git+ location: https://github.com/seatgeek/docker-build-cacher+ executable docker-build-cacher- hs-source-dirs: app- main-is: Main.hs- ghc-options: -threaded -rtsopts -with-rtsopts=-N- build-depends: base >= 4.9.1.0 && < 5- , turtle- , language-docker- , containers- , foldl- , text- , system-filepath- default-language: Haskell2010+ main-is: Main.hs+ hs-source-dirs:+ app+ ghc-options: -threaded -rtsopts -with-rtsopts=-N -Wall -fno-warn-unused-do-bind+ build-depends:+ base >=4.9.1.0 && <5+ , containers+ , foldl+ , language-docker >=2.0.1+ , system-filepath+ , text+ , turtle+ other-modules:+ Paths_docker_build_cacher+ default-language: Haskell2010