railpack

Configure and troubleshoot Railpack builds, with emphasis on RAILPACK_* environment variables, railpack.json overlays, build-plan inspection, local CLI installation and usage, and local BuildKit containers. Use for Railpack provider configuration, custom install/build/start commands, Mise or Apt packages, build or runtime variables and secrets, generated-plan debugging, BUILDKIT_HOST errors, or running Railpack from a release or source checkout.

Install
npx skills add 'https://github.com/railwayapp/railpack/tree/main/.'
Incomplete bundle · no download
main · 21a254fScanned 2026-09-17

Contributors

GitHub-linked commit authors for this SKILL.md at the saved revision. Co-authors and history before file renames are not included.

File history ↗

buildkit/build_llb/layers.go

buildkit/build_llb/layers.goBrowse 1970 files
View on GitHub
← Back to SKILL.md
package build_llb import (	"fmt"	"path"	"path/filepath"	"slices"	"strings" 	"github.com/charmbracelet/log"	"github.com/moby/buildkit/client/llb"	"github.com/railwayapp/railpack/core/plan") // GetStateForLayer returns the llb.State for a given layer not including any filters (include/exclude)func (g *BuildGraph) GetStateForLayer(layer plan.Layer) llb.State {	var state llb.State 	if layer.Image != "" {		state = llb.Image(layer.Image, llb.Platform(*g.Platform))	} else if layer.Local {		state = *g.LocalState	} else if layer.Step != "" {		if node, exists := g.graph.GetNode(layer.Step); exists {			nodeState := node.(*StepNode).State			if nodeState == nil {				return llb.Scratch()			}			state = *nodeState		}	} else {		state = llb.Scratch()	} 	return state} // GetFullStateFromLayers returns the llb.State for a given list of layers including any filters (include/exclude)// This will attempt to use an llb.Merge operation if possible, otherwise it will use an llb.Copy operation//// 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 size// We try to detect if there are overlaps and fallback to copy everything onto the base state (first layer)func (g *BuildGraph) GetFullStateFromLayers(layers []plan.Layer) llb.State {	if len(layers) == 0 {		return llb.Scratch()	} 	if len(layers[0].Include)+len(layers[0].Exclude) > 0 {		panic("first input must not have include or exclude paths")	} 	// Get the base state from the first input	state := g.GetStateForLayer(layers[0])	if len(layers) == 1 {		return state	} 	shouldMerge := shouldLLBMerge(layers)	if shouldMerge {		return g.getMergeState(layers)	} 	return g.getCopyState(layers)} func (g *BuildGraph) getCopyState(layers []plan.Layer) llb.State {	state := g.GetStateForLayer(layers[0])	if len(layers) == 1 {		return state	} 	for _, input := range layers[1:] {		inputState := g.GetStateForLayer(input)		state = copyLayerPaths(state, inputState, input.Filter, input.Local)	}	return state} func (g *BuildGraph) getMergeState(layers []plan.Layer) llb.State {	mergeStates := []llb.State{g.GetStateForLayer(layers[0])}	mergeNames := []string{layers[0].DisplayName()} 	for _, input := range layers[1:] {		if len(input.Include) == 0 {			log.Warnf("input %s has no include or exclude paths. This is probably a mistake.", input.Step)		}		inputState := g.GetStateForLayer(input)		destState := copyLayerPaths(llb.Scratch(), inputState, input.Filter, input.Local)		mergeStates = append(mergeStates, destState)		mergeNames = append(mergeNames, input.DisplayName())	} 	return llb.Merge(mergeStates, llb.WithCustomNamef("[railpack] merge %s", strings.Join(mergeNames, ", ")))} // copyLayerPaths copies paths from srcState to destState, applying the given filter.// If isLocal is true, files are copied from local filesystem into /app directory.// Otherwise paths are copied directly between container locations.func copyLayerPaths(destState, srcState llb.State, filter plan.Filter, isLocal bool) llb.State {	for _, include := range filter.Include {		srcPath, destPath := resolvePaths(include, isLocal) 		opts := []llb.ConstraintsOpt{}		if srcPath == destPath {			opts = append(opts, llb.WithCustomName(fmt.Sprintf("copy %s", srcPath)))		} 		destState = destState.File(llb.Copy(srcState, srcPath, destPath, &llb.CopyInfo{			CopyDirContentsOnly: true,			CreateDestPath:      true,			FollowSymlinks:      true,			AllowWildcard:       true,			AllowEmptyWildcard:  true,			ExcludePatterns:     filter.Exclude,		}), opts...)	}	return destState} // shouldLLBMerge determines if a set of layers should be merged based on path overlaps.// We should not merge layers if:// - The non-first layer has no include filters// - Any layer includes the root path "/"// - Any layer pulls from a local filesystem// - Any layer has overlapping paths with subsequent layers (unless excluded)func shouldLLBMerge(layers []plan.Layer) bool {	for i, layer := range layers {		if i != 0 && layer.Include == nil {			return false		} 		if slices.Contains(layer.Include, "/") {			return false		} 		if layer.Local {			return false		} 		for j := i + 1; j < len(layers); j++ {			if hasSignificantOverlap(layer, layers[j]) {				return false			}		}	}	return true} // hasSignificantOverlap checks if two layers have paths that would result in// actual data duplication. Overlaps that are covered by exclude patterns are not significant.func hasSignificantOverlap(layer1, layer2 plan.Layer) bool {	for _, p1 := range layer1.Include {		p1Clean := path.Clean(p1)		if p1Clean == "." {			p1Clean = "/app"		} else if !strings.HasPrefix(p1Clean, "/") {			p1Clean = path.Join("/app", p1Clean)		} 		for _, p2 := range layer2.Include {			p2Clean := path.Clean(p2)			if p2Clean == "." {				p2Clean = "/app"			} else if !strings.HasPrefix(p2Clean, "/") {				p2Clean = path.Join("/app", p2Clean)			} 			// Check if paths overlap			p1WithSlash := p1Clean + "/"			p2WithSlash := p2Clean + "/" 			var overlap bool			var innerPath, outerPath string			var outerExcludes []string 			if p1Clean == p2Clean {				// Exact match - always overlap				return true			} else if strings.HasPrefix(p1WithSlash, p2WithSlash) {				// p1 is inside p2 (e.g., /app/.nvmrc inside /app)				overlap = true				innerPath = p1Clean				outerPath = p2Clean				outerExcludes = layer2.Exclude			} else if strings.HasPrefix(p2WithSlash, p1WithSlash) {				// p2 is inside p1				overlap = true				innerPath = p2Clean				outerPath = p1Clean				outerExcludes = layer1.Exclude			} 			if overlap {				// Get the relative path from outer to inner				relPath := strings.TrimPrefix(innerPath, outerPath)				relPath = strings.TrimPrefix(relPath, "/") 				// Check if this relative path would be excluded				if isPathExcluded(relPath, outerExcludes) {					continue // Not a significant overlap				}				return true			}		}	}	return false} // isPathExcluded checks if a path matches any of the exclude patterns.// Patterns can match directory names at any level.func isPathExcluded(relPath string, excludes []string) bool {	if len(excludes) == 0 {		return false	} 	// Split the path into components	parts := strings.Split(relPath, "/") 	for _, exclude := range excludes {		// Check if any path component matches the exclude pattern		if slices.Contains(parts, exclude) {			return true		}		// Also check if the full relative path starts with the exclude		if strings.HasPrefix(relPath, exclude+"/") || relPath == exclude {			return true		}	}	return false} // hasPathOverlap checks if two slices of paths have any overlapping paths.// Paths overlap if they are identical or if one is a subdirectory of the other.// For example:////	hasPathOverlap([]string{"/app/dist"}, []string{"/app"}) // returns true//	hasPathOverlap([]string{"/app-foo"}, []string{"/app"}) // returns falsefunc hasPathOverlap(paths1, paths2 []string) bool {	for _, p1 := range paths1 {		p1Clean := path.Clean(p1)		if !strings.HasSuffix(p1Clean, "/") {			p1Clean = p1Clean + "/"		} 		for _, p2 := range paths2 {			p2Clean := path.Clean(p2)			if !strings.HasSuffix(p2Clean, "/") {				p2Clean = p2Clean + "/"			} 			// Check direct path match or if one is a subdirectory of the other			if p1Clean == p2Clean || strings.HasPrefix(p1Clean, p2Clean) || strings.HasPrefix(p2Clean, p1Clean) {				return true			}		}	}	return false} // resolvePaths determines source and destination paths based on the include path and whether it's local.// For local paths, only the basename is preserved when copying to /app directory.// For container paths, the full relative path structure is preserved under /app.func resolvePaths(include string, isLocal bool) (srcPath, destPath string) {	if isLocal {		// convert a local path reference to fully qualified container path		return include, filepath.Join("/app", filepath.Base(include))	} 	switch {	case include == "." || include == "/app" || include == "/app/":		return "/app", "/app"	case filepath.IsAbs(include):		return include, include	default:		return filepath.Join("/app", include), filepath.Join("/app", include)	}}