docker-build-cacher 1.1 → 1.7.1
raw patch · 2 files changed
+226/−189 lines, 2 files
Files
- app/Main.hs +225/−188
- docker-build-cacher.cabal +1/−1
app/Main.hs view
@@ -23,8 +23,8 @@ import Filesystem.Path (FilePath) import Language.Dockerfile hiding (Tag) import Prelude hiding (FilePath)-import Text.Read import Text.ParserCombinators.ReadP hiding (choice)+import Text.Read import Turtle {- Glossary:@@ -34,185 +34,187 @@ - cache buster: Files inside a docker image that can be compared with files locally under the same path -} newtype App =- App Text- deriving (Show)+ App Text+ deriving (Show) newtype Branch =- Branch Text- deriving (Show)+ Branch Text+ deriving (Show) newtype Tag =- Tag Text- deriving (Show, Eq)+ Tag Text+ deriving (Show, Eq) newtype SourcePath =- SourcePath FilePath- deriving (Show, Eq)+ SourcePath FilePath+ deriving (Show, Eq) newtype TargetPath =- TargetPath FilePath- deriving (Show, Eq)+ TargetPath FilePath+ deriving (Show, Eq) -- | A Stage is one of the FROM directives ina dockerfile -- data Stage = Stage- { stageName :: Text -- The image name- , stageTag :: Tag -- The image tag- , stagePos :: Linenumber -- Where in the docker file this line is found- , stageAlias :: Text -- The alias used for building- , 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)+ { stageName :: Text -- The image name+ , stageTag :: Tag -- The image tag+ , stagePos :: Linenumber -- Where in the docker file this line is found+ , stageAlias :: Text -- The alias used for building+ , 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- , cacheBusters :: [(SourcePath, TargetPath)] -- List of files that are able to bust the cache- }- deriving (Show)+ = 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+ , cacheBusters :: [(SourcePath, TargetPath)] -- List of files that are able to bust the cache+ }+ deriving (Show) -- | This script has 2 modes. One for building the Dockerfile, and another for caching its stages data Mode- = Build- | Cache- deriving (Show)+ = Build+ | Cache+ deriving (Show) instance Read Mode where- readPrec =- Text.Read.choice $- strValMap- [ ("Build", Build) -- Accept both casings- , ("build", Build)- , ("Cache", Cache)- , ("cache", Cache)- ]- where- strValMap = map (\(x, y) -> lift $ string x >> return y)+ readPrec =+ Text.Read.choice $+ strValMap+ [ ("Build", Build) -- Accept both casings+ , ("build", Build)+ , ("Cache", Cache)+ , ("cache", Cache)+ ]+ where+ strValMap = map (\(x, y) -> lift $ string x >> return y) -- | Describes the arguments this script takes from the command line parser :: Parser Mode parser = argRead "mode" "Whether to build or to cache (options: build | cache)" main = do- mode <- options "Builds a docker file and caches its stages" parser -- Parse the CLI arguments as a Mode- app <- App <$> needEnv "APP_NAME" -- Get the APP environment variable and then wrap it in App- branch <- Branch <$> needEnv "GIT_BRANCH"- maybeFile <-- do f <- need "DOCKERFILE" -- Get the dockerfile path if any- return (fmap fromText f) -- And transform it to a FilePath+ mode <- options "Builds a docker file and caches its stages" parser -- Parse the CLI arguments as a Mode+ app <- App <$> needEnv "APP_NAME" -- Get the APP environment variable and then wrap it in App+ branch <- Branch <$> needEnv "GIT_BRANCH"+ maybeFile <-+ do f <- need "DOCKERFILE" -- Get the dockerfile path if any+ return (fmap fromText f) -- And transform it to a FilePath -- -- if DOCKERFILE is not present, we assume is in the current directory- currentDirectory <- pwd- let Just file = maybeFile <|> Just (currentDirectory </> "Dockerfile")+ currentDirectory <- pwd+ let Just file = maybeFile <|> Just (currentDirectory </> "Dockerfile") -- -- Now we try to parse the dockefile- dockerFile <- parseFile (Text.unpack (format fp file)) -- Convert the dockerfile to an AST- case dockerFile of- Left message -> error ("There was an error parsing the docker file: " <> show message)- Right ast ->- case mode of- Cache -> sh (cacheBuild app branch ast)- Build -> do- tag <- Tag <$> needEnv "DOCKER_TAG"- sh (build app branch tag ast)+ dockerFile <- parseFile (Text.unpack (format fp file)) -- Convert the dockerfile to an AST+ case dockerFile of+ Left message -> error ("There was an error parsing the docker file: " <> show message)+ Right ast ->+ case mode of+ Cache -> sh (cacheBuild app branch ast)+ Build -> do+ tag <- Tag <$> needEnv "DOCKER_TAG"+ sh (build app branch tag ast) -- | 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. build :: App -> Branch -> Tag -> Dockerfile -> Shell () build app branch tag ast = do- changedStages <- getChangedStages app branch ast -- Inspect the dockerfile and return the stages that got their cache invalidated- echo "I'll start building now the main Dockerfile"- let bustedStages = replaceStages (filter alreadyCached changedStages) ast -- We replace the busted stages with cached primed ones- status <- buildDockerfile app tag bustedStages -- Build the main docker file with the maybe changed stages- case status of- ExitSuccess -> do- echo- "I built the main dockerfile without a problem. Now call this same script with the `Cache` mode"- ExitFailure _ -> do- die "Boo, I could not build the project"+ changedStages <- getChangedStages app branch ast -- Inspect the dockerfile and return the stages that got their cache invalidated+ echo "I'll start building now the main Dockerfile"+ let bustedStages = replaceStages (filter alreadyCached changedStages) ast -- We replace the busted stages with cached primed ones+ status <- buildDockerfile app tag bustedStages -- Build the main docker file with the maybe changed stages+ case status of+ ExitSuccess ->+ echo+ "I built the main dockerfile without a problem. Now call this same script with the `Cache` mode"+ ExitFailure _ -> die "Boo, I could not build the project" -- | One a dockefile is built, we can extrac each of the stages separately and then tag them, so the cache -- 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- echo "--> Let's make the cache great again"- mapM_ (buildAssetStage app) changedStages -- Build each of the stages so they can be reused later+ changedStages <- getChangedStages app branch ast+ when (changedStages /= []) $ do+ echo "--> Let's make the cache great again"+ mapM_ (buildAssetStage app) changedStages -- 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 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- when (length assetStages > length stages) showAliasesWarning+ 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+ 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+ bustCacheStages <- fold (getAlreadyCached stages >>= inspectCache >>= shouldBustCache) Fold.list+ return (catMaybes bustCacheStages) -- Remove the stages that did not actually change where showAliasesWarning = do- echo "::::::WARNING::::::"- echo "I found some FROM directives in the dockerfile that did not have an `as` alias"- echo "I'm not smart enough to build multi-stage docker files without aliases."- echo "While this is safe to do, you will get no cache benefits"- echo ""- echo "Please always write your FROM directives as `FROM image:tag as myalias`"+ echo "::::::WARNING::::::"+ echo "I found some FROM directives in the dockerfile that did not have an `as` alias"+ echo "I'm not smart enough to build multi-stage docker files without aliases."+ echo "While this is safe to do, you will get no cache benefits"+ echo ""+ echo "Please always write your FROM directives as `FROM image:tag as myalias`" -- | 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 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)- existent <-- fold- (inproc "docker" ["image", "ls", taggedBuild stageTag, "--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})- else do- echo "------> It already exists, so I will then check if the cache files changed"- return (stage {alreadyCached = True})+ 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)+ existent <-+ fold+ (inproc+ "docker"+ ["image", "ls", taggedBuild stageTag, "--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})+ else do+ echo "------> It already exists, so I will then check if the cache files changed"+ return (stage {alreadyCached = True}) -- | 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 {..}) = do- case alreadyCached of- False -> return (NotCached stage)- True -> 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 {..})+ case alreadyCached of+ False -> return (NotCached stage)+ True -> 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 {..}) where extractCachePaths workdir dir =- let filePairs = concat (mapMaybe doExtract dir) -- Get the (source, target) pairs of files copied- in fmap (prependWorkdir workdir) filePairs -- Some target paths need to have the WORKDIR prepended+ let filePairs = concat (mapMaybe doExtract dir) -- Get the (source, target) pairs of files copied+ in fmap (prependWorkdir workdir) filePairs -- Some target paths need to have the WORKDIR prepended -- -- | 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)+ 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) -- -- | COPY allows multiple paths in the same line, we need to convert each to a separate path doExtract (InstructionPos (Copy fr 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)))+ 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))) -- -- | 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)]+ Just [((toSource . Text.pack) fr, (toTarget . Text.pack) t)] doExtract _ = Nothing getFirst (first:_) = first getFirst [] = []@@ -222,48 +224,66 @@ shouldBustCache :: InspectedStage -> Shell (Maybe Stage) shouldBustCache (NotCached stage) = return (Just stage) shouldBustCache Cached {..} = do- printLn ("----> Checking cache buster files for stage " %s) (stageName stage)- containerId <- inproc "docker" ["create", (taggedBuild (stageTag stage))] empty- -- Get the cache buster files that have changed since last time- hasChanged <- fold (mfilter isJust (checkFileChanged containerId cacheBusters)) Fold.head- if isJust hasChanged- then do- printLn ("----> The stage " %s % " changed") (stageName stage)- return (Just stage)- else do- printLn ("----> The stage " %s % " did not change") (stageName stage)- return Nothing+ printLn ("----> Checking cache buster files for stage " %s) (stageName stage)+ withContainer stage checkFiles -- Create a container to inspect the files where+ checkFiles containerId = do+ hasChanged <- fold (mfilter isJust (checkFileChanged containerId cacheBusters)) Fold.head+ -- ^ Get the cache buster files that have changed since last time+ -- The following is executed for each of the files found+ if isJust hasChanged+ then do+ printLn ("----> The stage " %s % " changed") (stageName stage)+ return (Just stage)+ else do+ printLn ("----> The stage " %s % " did not change") (stageName stage)+ return Nothing+ -- | checkFileChanged containerId files = do- (SourcePath file, TargetPath targetDir) <- select files- printLn ("------> Checking file '" %fp % "' in directory " %fp) file targetDir- currentDirectory <- pwd- tempFile <- mktempfile currentDirectory "comp"- let targetFile = targetDir </> file- status <-- proc "docker" ["cp", format (l % ":" %fp) containerId targetFile, format fp tempFile] empty- guard (status == ExitSuccess)- local <- liftIO (readTextFile file)- remote <- liftIO (readTextFile tempFile)- if local == remote- then return Nothing- else return (Just file)+ (SourcePath file, TargetPath targetDir) <- select files+ printLn ("------> Checking file '" %fp % "' in directory " %fp) file targetDir+ currentDirectory <- pwd+ tempFile <- mktempfile currentDirectory "comp"+ let targetFile = targetDir </> file+ status <-+ proc+ "docker"+ ["cp", format (s % ":" %fp) containerId targetFile, format fp tempFile]+ empty+ guard (status == ExitSuccess)+ local <- liftIO (readTextFile file)+ remote <- liftIO (readTextFile tempFile)+ if local == remote+ then return Nothing+ else return (Just file) +-- | 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+ result <- fold (action (format l containerId)) Fold.list+ removeContainer containerId+ select result -- yield each result as a separate line+ where+ removeContainer containerId =+ proc "docker" ["rm", format l containerId] empty+ -- | 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 {..} = do- printLn ("\n--> Building asset stage " %s % " at line " %d) stageName stagePos- let filteredDirectives = filter isFrom directives- status <- buildDockerfile app stageTag filteredDirectives -- 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)) 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!"+ printLn ("\n--> Building asset stage " %s % " at line " %d) stageName stagePos+ let filteredDirectives = filter isFrom directives+ status <- buildDockerfile app stageTag filteredDirectives -- 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)) 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@@ -271,24 +291,34 @@ -- | Simply call docker build for the passed arguments buildDockerfile :: App -> Tag -> Dockerfile -> Shell ExitCode buildDockerfile (App app) (Tag tag) directives = do- currentDirectory <- pwd- tmpFile <- mktempfile currentDirectory "Dockerfile."- liftIO (writeTextFile tmpFile (Text.pack (prettyPrint directives))) -- Put the Dockerfile contents in the tmp file- proc- "docker"- ["build", "--build-arg", "APP_NAME=" <> app, "-f", format fp tmpFile, "-t", tag, "."]- empty -- Build the generated dockerfile+ currentDirectory <- pwd+ tmpFile <- mktempfile currentDirectory "Dockerfile."+ liftIO (writeTextFile tmpFile (Text.pack (prettyPrint directives))) -- Put the Dockerfile contents in the tmp file+ proc+ "docker"+ [ "build"+ , "--network"+ , "host"+ , "--build-arg"+ , "APP_NAME=" <> app+ , "-f"+ , format fp tmpFile+ , "-t"+ , tag+ , "."+ ]+ empty -- Build the generated dockerfile -- | 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 tag) onBuildLines = do- let eitherDirectives = map (parseString . Text.unpack) onBuildLines- file =- toDockerfile $ do- from (tagged (Text.unpack tag) "latest")- mapM (onBuildRaw . toInstruction) (rights eitherDirectives) -- Append each of the ONBUILD instructions- return file+ let eitherDirectives = map (parseString . Text.unpack) onBuildLines+ file =+ toDockerfile $ do+ from (tagged (Text.unpack tag) "latest")+ mapM (onBuildRaw . toInstruction) (rights eitherDirectives) -- Append each of the ONBUILD instructions+ return file where toInstruction [(InstructionPos inst _ _)] = inst toInstruction _ = error "This is not possible"@@ -309,15 +339,15 @@ isOnBuild line = Text.isPrefixOf onBuildPrefix (lineToText line) extractWorkdir :: [Line] -> FilePath-extractWorkdir lines =- case reverse (doExtract lines) of- [] -> fromText "/"- lastWorkdir:_ -> fromText lastWorkdir -- We are on;y intereted in the latest WORKDIR declared+extractWorkdir instructions =+ case reverse (doExtract instructions) of+ [] -> fromText "/"+ lastWorkdir:_ -> fromText lastWorkdir -- We are on;y intereted in the latest WORKDIR declared where onBuildPrefix = "/bin/sh -c #(nop) ONBUILD " -- Curiously, ONBUILD is lead by 2 spaces workdirPrefix = "/bin/sh -c #(nop) WORKDIR " -- Whereas WORKDIR only by one space --- -- | First find all relevant lines, then kepp the lines starting with the workdir prefix+ -- | First find all relevant instructions, then keep the lines starting with the workdir prefix doExtract = mapMaybe (Text.stripPrefix workdirPrefix . lineToText) . findRelevantLines -- -- | Take lines until a ONBUILD is found@@ -328,12 +358,12 @@ -- | Calls docker history for the given image name and returns the output as a list imageHistory :: Tag -> 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- return (reverse out) -- The history comes in reverse order, sort it naturally+ printLn ("----> Checking the docker image history for " %s) tag+ out <- fold fetchHistory Fold.list -- Buffer all the output of the imageHistory shell+ return (reverse out) -- The history comes in reverse order, sort it naturally where fetchHistory =- inproc "docker" ["history", "--no-trunc", "--format", "{{.CreatedBy}}", tag] empty+ inproc "docker" ["history", "--no-trunc", "--format", "{{.CreatedBy}}", tag] empty -- -- | Returns a list of directives grouped by the appeareance of the FROM directive@@ -353,17 +383,17 @@ -- | Converts a list of instructions into a Stage record toStage :: App -> Branch -> Dockerfile -> Maybe Stage toStage (App app) (Branch branch) directives = do- (stageName, stagePos, stageAlias) <- getStageInfo directives -- If getStageInfo returns Nothing, skip the rest- let sanitized = sanitize stageName- stageTag = Tag (app <> "__branch__" <> branch <> "__stage__" <> sanitized)- alreadyCached = False- return Stage {..}+ (stageName, stagePos, stageAlias) <- getStageInfo directives -- If getStageInfo returns Nothing, skip the rest+ let sanitized = sanitize stageName+ stageTag = Tag (app <> "__branch__" <> branch <> "__stage__" <> sanitized)+ alreadyCached = False+ return Stage {..} where getStageInfo :: [InstructionPos] -> Maybe (Text, Linenumber, Text) getStageInfo ((InstructionPos (From (TaggedImage name tag)) _ pos):_) =- if Text.isInfixOf " as " (Text.pack tag) -- Make sure the FROM is aliased- then Just (Text.pack name, pos, parseAlias tag)- else Nothing+ 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 -- -- | Makes a string safe to use it as a file name@@ -371,7 +401,13 @@ -- | Extracts the stage alias out of the FROM directive parseAlias :: String -> Text-parseAlias = Text.strip . Text.replace "as " "" . snd . Text.breakOn " as " . Text.pack+parseAlias =+ Text.strip .+ Text.replace "as " "" . -- Remove the alias+ snd .+ Text.breakOn " as " . -- Split by the alias name and get the second in the tuple+ Text.replace " AS " " as " . -- Normalize AS with as+ Text.pack taggedBuild :: Tag -> Text taggedBuild (Tag tag) = tag <> "-build"@@ -387,13 +423,14 @@ -- | 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+ 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 = directive formatAlias alias = "latest as " <> Text.unpack alias @@ -406,7 +443,7 @@ toTarget = TargetPath . fromText needEnv varName = do- value <- need varName- case value of- Nothing -> error ("I was expecting the " <> show varName <> " env var to be present.")- Just val -> return val+ value <- need varName+ case value of+ Nothing -> error ("I was expecting the " <> show varName <> " env var to be present.")+ Just val -> return val
docker-build-cacher.cabal view
@@ -1,5 +1,5 @@ name: docker-build-cacher-version: 1.1+version: 1.7.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 stages separately, so the can be used in successive builds.