345 lines
10 KiB
Go
345 lines
10 KiB
Go
package buildpack
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/BurntSushi/toml"
|
|
|
|
"github.com/buildpacks/lifecycle/api"
|
|
"github.com/buildpacks/lifecycle/env"
|
|
"github.com/buildpacks/lifecycle/internal/encoding"
|
|
"github.com/buildpacks/lifecycle/launch"
|
|
"github.com/buildpacks/lifecycle/layers"
|
|
"github.com/buildpacks/lifecycle/log"
|
|
)
|
|
|
|
const (
|
|
// EnvBpPlanPath is the absolute path of the filtered build plan, containing relevant Buildpack Plan entries from detection
|
|
EnvBpPlanPath = "CNB_BP_PLAN_PATH"
|
|
// EnvLayersDir is the absolute path of the buildpack layers directory (read-write); a different copy is provided for each buildpack;
|
|
// contents may be saved to either or both of: the final output image or the cache
|
|
EnvLayersDir = "CNB_LAYERS_DIR"
|
|
// Also provided during build: EnvBuildpackDir, EnvPlatformDir, EnvExecEnv (see detect.go)
|
|
)
|
|
|
|
type BuildInputs struct {
|
|
AppDir string
|
|
BuildConfigDir string
|
|
LayersDir string
|
|
PlatformDir string
|
|
Env BuildEnv
|
|
TargetEnv []string
|
|
ExecEnv string
|
|
Out, Err io.Writer
|
|
Plan Plan
|
|
}
|
|
|
|
type BuildEnv interface {
|
|
AddRootDir(baseDir string) error
|
|
AddEnvDir(envDir string, defaultAction env.ActionType) error
|
|
WithOverrides(platformDir string, buildConfigDir string) ([]string, error)
|
|
List() []string
|
|
}
|
|
|
|
type BuildOutputs struct {
|
|
BOMFiles []BOMFile
|
|
BuildBOM []BOMEntry
|
|
Labels []Label
|
|
LaunchBOM []BOMEntry
|
|
MetRequires []string
|
|
Processes []launch.Process
|
|
Slices []layers.Slice
|
|
}
|
|
|
|
// BuildExecutor executes a single buildpack's `./bin/build` binary,
|
|
// providing inputs as defined in the Buildpack Interface Specification,
|
|
// and processing outputs for the platform.
|
|
//
|
|
//go:generate mockgen -package testmock -destination ../phase/testmock/build_executor.go github.com/buildpacks/lifecycle/buildpack BuildExecutor
|
|
type BuildExecutor interface {
|
|
Build(d BpDescriptor, inputs BuildInputs, logger log.Logger) (BuildOutputs, error)
|
|
}
|
|
|
|
type DefaultBuildExecutor struct{}
|
|
|
|
func (e *DefaultBuildExecutor) Build(d BpDescriptor, inputs BuildInputs, logger log.Logger) (BuildOutputs, error) {
|
|
logger.Debug("Creating plan directory")
|
|
planDir, err := os.MkdirTemp("", launch.EscapeID(d.Buildpack.ID)+"-")
|
|
if err != nil {
|
|
return BuildOutputs{}, err
|
|
}
|
|
defer os.RemoveAll(planDir)
|
|
|
|
logger.Debug("Preparing paths")
|
|
bpLayersDir, planPath, err := prepareInputPaths(d.Buildpack.ID, inputs.Plan, inputs.LayersDir, planDir)
|
|
if err != nil {
|
|
return BuildOutputs{}, err
|
|
}
|
|
|
|
logger.Debug("Running build command")
|
|
if err := runBuildCmd(d, bpLayersDir, planPath, inputs, inputs.Env); err != nil {
|
|
return BuildOutputs{}, err
|
|
}
|
|
|
|
logger.Debug("Processing layers")
|
|
createdLayers, err := d.processLayers(bpLayersDir, logger)
|
|
if err != nil {
|
|
return BuildOutputs{}, err
|
|
}
|
|
|
|
logger.Debug("Updating environment")
|
|
if err := d.setupEnv(bpLayersDir, createdLayers, inputs.Env); err != nil {
|
|
return BuildOutputs{}, err
|
|
}
|
|
|
|
logger.Debug("Reading output files")
|
|
return d.readOutputFilesBp(bpLayersDir, planPath, inputs.Plan, createdLayers, logger)
|
|
}
|
|
|
|
func prepareInputPaths(bpID string, plan Plan, layersDir, parentPlanDir string) (string, string, error) {
|
|
bpDirName := launch.EscapeID(bpID) // FIXME: this logic should eventually move to the platform package
|
|
|
|
// Create e.g., <layers>/<buildpack-id> or <output>/<extension-id>
|
|
bpLayersDir := filepath.Join(layersDir, bpDirName)
|
|
if err := os.MkdirAll(bpLayersDir, 0777); err != nil {
|
|
return "", "", err
|
|
}
|
|
|
|
// Create Buildpack Plan
|
|
childPlanDir := filepath.Join(parentPlanDir, bpDirName) // FIXME: it's unclear if this child directory is necessary; consider removing
|
|
if err := os.MkdirAll(childPlanDir, 0777); err != nil {
|
|
return "", "", err
|
|
}
|
|
planPath := filepath.Join(childPlanDir, "plan.toml")
|
|
if err := encoding.WriteTOML(planPath, plan); err != nil {
|
|
return "", "", err
|
|
}
|
|
|
|
return bpLayersDir, planPath, nil
|
|
}
|
|
|
|
func runBuildCmd(d BpDescriptor, bpLayersDir, planPath string, inputs BuildInputs, buildEnv BuildEnv) error {
|
|
cmd := exec.Command(
|
|
filepath.Join(d.WithRootDir, "bin", "build"),
|
|
bpLayersDir,
|
|
inputs.PlatformDir,
|
|
planPath,
|
|
) // #nosec G204
|
|
cmd.Dir = inputs.AppDir
|
|
cmd.Stdout = inputs.Out
|
|
cmd.Stderr = inputs.Out
|
|
|
|
var err error
|
|
if d.Buildpack.ClearEnv {
|
|
cmd.Env, err = buildEnv.WithOverrides("", inputs.BuildConfigDir)
|
|
} else {
|
|
cmd.Env, err = buildEnv.WithOverrides(inputs.PlatformDir, inputs.BuildConfigDir)
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
cmd.Env = append(cmd.Env, EnvBuildpackDir+"="+d.WithRootDir)
|
|
if api.MustParse(d.WithAPI).AtLeast("0.8") {
|
|
cmd.Env = append(cmd.Env,
|
|
EnvPlatformDir+"="+inputs.PlatformDir,
|
|
EnvBpPlanPath+"="+planPath,
|
|
EnvLayersDir+"="+bpLayersDir,
|
|
)
|
|
}
|
|
if api.MustParse(d.API()).AtLeast("0.10") {
|
|
cmd.Env = append(cmd.Env, inputs.TargetEnv...)
|
|
}
|
|
if api.MustParse(d.API()).AtLeast("0.12") && inputs.ExecEnv != "" {
|
|
cmd.Env = append(cmd.Env, "CNB_EXEC_ENV="+inputs.ExecEnv)
|
|
}
|
|
|
|
if err = cmd.Run(); err != nil {
|
|
return NewError(err, ErrTypeBuildpack)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (d BpDescriptor) processLayers(bpLayersDir string, logger log.Logger) (map[string]LayerMetadataFile, error) {
|
|
bpLayers := make(map[string]LayerMetadataFile)
|
|
if err := eachLayer(bpLayersDir, func(layerPath string) error {
|
|
layerFile, err := DecodeLayerMetadataFile(layerPath+".toml", d.WithAPI, logger)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to decode layer metadata file: %w", err)
|
|
}
|
|
if err = renameLayerDirIfNeeded(layerFile, layerPath); err != nil {
|
|
return fmt.Errorf("failed to rename layer directory: %w", err)
|
|
}
|
|
bpLayers[layerPath] = layerFile
|
|
return nil
|
|
}); err != nil {
|
|
return nil, fmt.Errorf("failed to process buildpack layer: %w", err)
|
|
}
|
|
return bpLayers, nil
|
|
}
|
|
|
|
func eachLayer(bpLayersDir string, fn func(layerPath string) error) error {
|
|
files, err := os.ReadDir(bpLayersDir)
|
|
if os.IsNotExist(err) {
|
|
return nil
|
|
} else if err != nil {
|
|
return err
|
|
}
|
|
for _, f := range files {
|
|
if f.IsDir() || !strings.HasSuffix(f.Name(), ".toml") {
|
|
continue
|
|
}
|
|
path := filepath.Join(bpLayersDir, strings.TrimSuffix(f.Name(), ".toml"))
|
|
if err = fn(path); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func renameLayerDirIfNeeded(layerMetadataFile LayerMetadataFile, layerDir string) error {
|
|
// rename <layers>/<layer> to <layers>/<layer>.ignore if all the types flags are set to false
|
|
if !layerMetadataFile.Launch && !layerMetadataFile.Cache && !layerMetadataFile.Build {
|
|
if err := os.Rename(layerDir, layerDir+".ignore"); err != nil && !os.IsNotExist(err) {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (d BpDescriptor) setupEnv(bpLayersDir string, createdLayers map[string]LayerMetadataFile, buildEnv BuildEnv) error {
|
|
bpAPI := api.MustParse(d.WithAPI)
|
|
return eachLayer(bpLayersDir, func(layerPath string) error {
|
|
var err error
|
|
layerMetadataFile, ok := createdLayers[layerPath]
|
|
if !ok {
|
|
return fmt.Errorf("failed to find layer metadata for %s", layerPath)
|
|
}
|
|
if !layerMetadataFile.Build {
|
|
return nil
|
|
}
|
|
if err = buildEnv.AddRootDir(layerPath); err != nil {
|
|
return err
|
|
}
|
|
if err = buildEnv.AddEnvDir(filepath.Join(layerPath, "env"), env.DefaultActionType(bpAPI)); err != nil {
|
|
return err
|
|
}
|
|
return buildEnv.AddEnvDir(filepath.Join(layerPath, "env.build"), env.DefaultActionType(bpAPI))
|
|
})
|
|
}
|
|
|
|
func (d BpDescriptor) readOutputFilesBp(bpLayersDir, bpPlanPath string, bpPlanIn Plan, bpLayers map[string]LayerMetadataFile, logger log.Logger) (BuildOutputs, error) {
|
|
br := BuildOutputs{}
|
|
bpFromBpInfo := GroupElement{ID: d.Buildpack.ID, Version: d.Buildpack.Version}
|
|
|
|
// setup launch.toml
|
|
var launchTOML LaunchTOML
|
|
launchPath := filepath.Join(bpLayersDir, "launch.toml")
|
|
|
|
bomValidator := NewBOMValidator(d.WithAPI, bpLayersDir, logger)
|
|
|
|
var err error
|
|
// read build.toml
|
|
var buildTOML BuildTOML
|
|
buildPath := filepath.Join(bpLayersDir, "build.toml")
|
|
if _, err := toml.DecodeFile(buildPath, &buildTOML); err != nil && !os.IsNotExist(err) {
|
|
return BuildOutputs{}, err
|
|
}
|
|
if _, err := bomValidator.ValidateBOM(bpFromBpInfo, buildTOML.BOM); err != nil {
|
|
return BuildOutputs{}, err
|
|
}
|
|
br.BuildBOM, err = bomValidator.ValidateBOM(bpFromBpInfo, buildTOML.BOM)
|
|
if err != nil {
|
|
return BuildOutputs{}, err
|
|
}
|
|
|
|
// set MetRequires
|
|
if err := validateUnmet(buildTOML.Unmet, bpPlanIn); err != nil {
|
|
return BuildOutputs{}, err
|
|
}
|
|
br.MetRequires = names(bpPlanIn.filter(buildTOML.Unmet).Entries)
|
|
|
|
// set BOM files
|
|
br.BOMFiles, err = d.processSBOMFiles(bpLayersDir, bpFromBpInfo, bpLayers, logger)
|
|
if err != nil {
|
|
return BuildOutputs{}, err
|
|
}
|
|
|
|
// read launch.toml, return if not exists
|
|
if err := DecodeLaunchTOML(launchPath, d.WithAPI, &launchTOML); os.IsNotExist(err) {
|
|
return br, nil
|
|
} else if err != nil {
|
|
return BuildOutputs{}, err
|
|
}
|
|
|
|
// set BOM
|
|
br.LaunchBOM, err = bomValidator.ValidateBOM(bpFromBpInfo, launchTOML.BOM)
|
|
if err != nil {
|
|
return BuildOutputs{}, err
|
|
}
|
|
|
|
if err := validateNoMultipleDefaults(launchTOML.Processes); err != nil {
|
|
return BuildOutputs{}, err
|
|
}
|
|
|
|
// set data from launch.toml
|
|
br.Labels = append([]Label{}, launchTOML.Labels...)
|
|
for i := range launchTOML.Processes {
|
|
if api.MustParse(d.WithAPI).LessThan("0.8") {
|
|
if launchTOML.Processes[i].WorkingDirectory != "" {
|
|
logger.Warn(fmt.Sprintf("Warning: process working directory isn't supported in this buildpack api version. Ignoring working directory for process '%s'", launchTOML.Processes[i].Type))
|
|
launchTOML.Processes[i].WorkingDirectory = ""
|
|
}
|
|
}
|
|
}
|
|
br.Processes = append([]launch.Process{}, launchTOML.ToLaunchProcessesForBuildpack(d.Buildpack.ID)...)
|
|
br.Slices = append([]layers.Slice{}, launchTOML.Slices...)
|
|
|
|
return br, nil
|
|
}
|
|
|
|
func names(requires []Require) []string {
|
|
var out []string
|
|
for _, req := range requires {
|
|
out = append(out, req.Name)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func validateUnmet(unmet []Unmet, bpPlan Plan) error {
|
|
for _, unmet := range unmet {
|
|
if unmet.Name == "" {
|
|
return errors.New("unmet.name is required")
|
|
}
|
|
found := false
|
|
for _, req := range bpPlan.Entries {
|
|
if unmet.Name == req.Name {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
return fmt.Errorf("unmet.name '%s' must match a requested dependency", unmet.Name)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateNoMultipleDefaults(processes []ProcessEntry) error {
|
|
defaultType := ""
|
|
for _, process := range processes {
|
|
if process.Default && defaultType != "" {
|
|
return fmt.Errorf("multiple default process types aren't allowed")
|
|
}
|
|
if process.Default {
|
|
defaultType = process.Type
|
|
}
|
|
}
|
|
return nil
|
|
}
|