buildkit/build_llb/layers.go
buildkit/build_llb/layers.goBrowse 1970 files
2,236 tokens
8,288 bytes
Token encoding: o200k_base
Snapshot 21a254f
← Back to SKILL.md
1package build_llb2 3import (4 "fmt"5 "path"6 "path/filepath"7 "slices"8 "strings"9 10 "github.com/charmbracelet/log"11 "github.com/moby/buildkit/client/llb"12 "github.com/railwayapp/railpack/core/plan"13)14 15// GetStateForLayer returns the llb.State for a given layer not including any filters (include/exclude)16func (g *BuildGraph) GetStateForLayer(layer plan.Layer) llb.State {17 var state llb.State18 19 if layer.Image != "" {20 state = llb.Image(layer.Image, llb.Platform(*g.Platform))21 } else if layer.Local {22 state = *g.LocalState23 } else if layer.Step != "" {24 if node, exists := g.graph.GetNode(layer.Step); exists {25 nodeState := node.(*StepNode).State26 if nodeState == nil {27 return llb.Scratch()28 }29 state = *nodeState30 }31 } else {32 state = llb.Scratch()33 }34 35 return state36}37 38// GetFullStateFromLayers returns the llb.State for a given list of layers including any filters (include/exclude)39// This will attempt to use an llb.Merge operation if possible, otherwise it will use an llb.Copy operation40//41// Merge is more efficient, but if the layers being merged overlap, the the data will be duplicated in the final image resulting in a larger image size42// We try to detect if there are overlaps and fallback to copy everything onto the base state (first layer)43func (g *BuildGraph) GetFullStateFromLayers(layers []plan.Layer) llb.State {44 if len(layers) == 0 {45 return llb.Scratch()46 }47 48 if len(layers[0].Include)+len(layers[0].Exclude) > 0 {49 panic("first input must not have include or exclude paths")50 }51 52 // Get the base state from the first input53 state := g.GetStateForLayer(layers[0])54 if len(layers) == 1 {55 return state56 }57 58 shouldMerge := shouldLLBMerge(layers)59 if shouldMerge {60 return g.getMergeState(layers)61 }62 63 return g.getCopyState(layers)64}65 66func (g *BuildGraph) getCopyState(layers []plan.Layer) llb.State {67 state := g.GetStateForLayer(layers[0])68 if len(layers) == 1 {69 return state70 }71 72 for _, input := range layers[1:] {73 inputState := g.GetStateForLayer(input)74 state = copyLayerPaths(state, inputState, input.Filter, input.Local)75 }76 return state77}78 79func (g *BuildGraph) getMergeState(layers []plan.Layer) llb.State {80 mergeStates := []llb.State{g.GetStateForLayer(layers[0])}81 mergeNames := []string{layers[0].DisplayName()}82 83 for _, input := range layers[1:] {84 if len(input.Include) == 0 {85 log.Warnf("input %s has no include or exclude paths. This is probably a mistake.", input.Step)86 }87 inputState := g.GetStateForLayer(input)88 destState := copyLayerPaths(llb.Scratch(), inputState, input.Filter, input.Local)89 mergeStates = append(mergeStates, destState)90 mergeNames = append(mergeNames, input.DisplayName())91 }92 93 return llb.Merge(mergeStates, llb.WithCustomNamef("[railpack] merge %s", strings.Join(mergeNames, ", ")))94}95 96// copyLayerPaths copies paths from srcState to destState, applying the given filter.97// If isLocal is true, files are copied from local filesystem into /app directory.98// Otherwise paths are copied directly between container locations.99func copyLayerPaths(destState, srcState llb.State, filter plan.Filter, isLocal bool) llb.State {100 for _, include := range filter.Include {101 srcPath, destPath := resolvePaths(include, isLocal)102 103 opts := []llb.ConstraintsOpt{}104 if srcPath == destPath {105 opts = append(opts, llb.WithCustomName(fmt.Sprintf("copy %s", srcPath)))106 }107 108 destState = destState.File(llb.Copy(srcState, srcPath, destPath, &llb.CopyInfo{109 CopyDirContentsOnly: true,110 CreateDestPath: true,111 FollowSymlinks: true,112 AllowWildcard: true,113 AllowEmptyWildcard: true,114 ExcludePatterns: filter.Exclude,115 }), opts...)116 }117 return destState118}119 120// shouldLLBMerge determines if a set of layers should be merged based on path overlaps.121// We should not merge layers if:122// - The non-first layer has no include filters123// - Any layer includes the root path "/"124// - Any layer pulls from a local filesystem125// - Any layer has overlapping paths with subsequent layers (unless excluded)126func shouldLLBMerge(layers []plan.Layer) bool {127 for i, layer := range layers {128 if i != 0 && layer.Include == nil {129 return false130 }131 132 if slices.Contains(layer.Include, "/") {133 return false134 }135 136 if layer.Local {137 return false138 }139 140 for j := i + 1; j < len(layers); j++ {141 if hasSignificantOverlap(layer, layers[j]) {142 return false143 }144 }145 }146 return true147}148 149// hasSignificantOverlap checks if two layers have paths that would result in150// actual data duplication. Overlaps that are covered by exclude patterns are not significant.151func hasSignificantOverlap(layer1, layer2 plan.Layer) bool {152 for _, p1 := range layer1.Include {153 p1Clean := path.Clean(p1)154 if p1Clean == "." {155 p1Clean = "/app"156 } else if !strings.HasPrefix(p1Clean, "/") {157 p1Clean = path.Join("/app", p1Clean)158 }159 160 for _, p2 := range layer2.Include {161 p2Clean := path.Clean(p2)162 if p2Clean == "." {163 p2Clean = "/app"164 } else if !strings.HasPrefix(p2Clean, "/") {165 p2Clean = path.Join("/app", p2Clean)166 }167 168 // Check if paths overlap169 p1WithSlash := p1Clean + "/"170 p2WithSlash := p2Clean + "/"171 172 var overlap bool173 var innerPath, outerPath string174 var outerExcludes []string175 176 if p1Clean == p2Clean {177 // Exact match - always overlap178 return true179 } else if strings.HasPrefix(p1WithSlash, p2WithSlash) {180 // p1 is inside p2 (e.g., /app/.nvmrc inside /app)181 overlap = true182 innerPath = p1Clean183 outerPath = p2Clean184 outerExcludes = layer2.Exclude185 } else if strings.HasPrefix(p2WithSlash, p1WithSlash) {186 // p2 is inside p1187 overlap = true188 innerPath = p2Clean189 outerPath = p1Clean190 outerExcludes = layer1.Exclude191 }192 193 if overlap {194 // Get the relative path from outer to inner195 relPath := strings.TrimPrefix(innerPath, outerPath)196 relPath = strings.TrimPrefix(relPath, "/")197 198 // Check if this relative path would be excluded199 if isPathExcluded(relPath, outerExcludes) {200 continue // Not a significant overlap201 }202 return true203 }204 }205 }206 return false207}208 209// isPathExcluded checks if a path matches any of the exclude patterns.210// Patterns can match directory names at any level.211func isPathExcluded(relPath string, excludes []string) bool {212 if len(excludes) == 0 {213 return false214 }215 216 // Split the path into components217 parts := strings.Split(relPath, "/")218 219 for _, exclude := range excludes {220 // Check if any path component matches the exclude pattern221 if slices.Contains(parts, exclude) {222 return true223 }224 // Also check if the full relative path starts with the exclude225 if strings.HasPrefix(relPath, exclude+"/") || relPath == exclude {226 return true227 }228 }229 return false230}231 232// hasPathOverlap checks if two slices of paths have any overlapping paths.233// Paths overlap if they are identical or if one is a subdirectory of the other.234// For example:235//236// hasPathOverlap([]string{"/app/dist"}, []string{"/app"}) // returns true237// hasPathOverlap([]string{"/app-foo"}, []string{"/app"}) // returns false238func hasPathOverlap(paths1, paths2 []string) bool {239 for _, p1 := range paths1 {240 p1Clean := path.Clean(p1)241 if !strings.HasSuffix(p1Clean, "/") {242 p1Clean = p1Clean + "/"243 }244 245 for _, p2 := range paths2 {246 p2Clean := path.Clean(p2)247 if !strings.HasSuffix(p2Clean, "/") {248 p2Clean = p2Clean + "/"249 }250 251 // Check direct path match or if one is a subdirectory of the other252 if p1Clean == p2Clean || strings.HasPrefix(p1Clean, p2Clean) || strings.HasPrefix(p2Clean, p1Clean) {253 return true254 }255 }256 }257 return false258}259 260// resolvePaths determines source and destination paths based on the include path and whether it's local.261// For local paths, only the basename is preserved when copying to /app directory.262// For container paths, the full relative path structure is preserved under /app.263func resolvePaths(include string, isLocal bool) (srcPath, destPath string) {264 if isLocal {265 // convert a local path reference to fully qualified container path266 return include, filepath.Join("/app", filepath.Base(include))267 }268 269 switch {270 case include == "." || include == "/app" || include == "/app/":271 return "/app", "/app"272 case filepath.IsAbs(include):273 return include, include274 default:275 return filepath.Join("/app", include), filepath.Join("/app", include)276 }277}278